+```
+
+```{toctree}
+---
+caption: Contribution guide
+maxdepth: 1
+---
+development/contribute
+development/environment
+development/documentation
+development/translation
+development/packaging
+development/testing
+development/history
+```
diff --git a/docs/push_translations.yml b/docs/push_translations.yml
deleted file mode 100644
index 325c665..0000000
--- a/docs/push_translations.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-name: Translations
-
-on:
- push:
- branches:
- - master
-
-jobs:
- push_translations:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v2
- with:
- submodules: true
-
- - name: Set up Python 3.8
- uses: actions/setup-python@v1
- with:
- python-version: 3.8
-
- - name: Install qgis-plugin-ci
- run: pip3 install qgis-plugin-ci
-
- - name: Push translations
- run: qgis-plugin-ci push-translation ${{ secrets.TRANSIFEX_TOKEN }}
diff --git a/docs/usage/installation.md b/docs/usage/installation.md
new file mode 100644
index 0000000..b62e77e
--- /dev/null
+++ b/docs/usage/installation.md
@@ -0,0 +1,19 @@
+# Installation
+
+# >> Stable version (recomended)
+
+This plugin is published on the official QGIS plugins repository: .
+
+# >> Beta versions released
+
+Enable experimental extensions in the QGIS plugins manager settings panel.
+
+# >> Earlier development version
+
+If you define yourself as early adopter or a tester and can't wait for the release, the plugin is automatically packaged for each commit to main, so you can use this address as repository URL in your QGIS extensions manager settings:
+
+```url
+https://github.com/lpoaura/PluginQGis-LPOData/plugins.xml
+```
+
+Be careful, this version can be unstable.
diff --git a/plugin_qgis_lpo.code-workspace b/plugin_qgis_lpo.code-workspace
deleted file mode 100644
index 96cd67e..0000000
--- a/plugin_qgis_lpo.code-workspace
+++ /dev/null
@@ -1,54 +0,0 @@
-{
- "folders": [
- {
- "path": "."
- }
- ],
- "settings": {
- "python.languageServer": "Jedi",
- "python.testing.pytestEnabled": true,
- "python.testing.pytestArgs": [
- "test"
- ],
- "python.testing.unittestEnabled": false,
- "python.testing.nosetestsEnabled": false,
- "python.linting.enabled": true,
- "python.linting.flake8Enabled": true,
- "python.linting.mypyEnabled": true,
- "python.linting.pylintEnabled": false,
- "python.defaultInterpreterPath": ".venv\\Scripts\\python.exe",
- "editor.formatOnSave": true,
- "python.analysis.autoFormatStrings": true,
- "isort.check": true,
- "editor.defaultFormatter": "ms-python.black-formatter",
- },
- "launch": {
- "configurations": [
- {
- "name": "QGIS debugpy",
- "type": "python",
- "request": "attach",
- "connect": {
- "host": "localhost",
- "port": 5678
- },
- "pathMappings": [
- {
- "localRoot": "${workspaceFolder}/plugin_qgis_lpo",
- "remoteRoot": "C:/Users/${env:USERNAME}/AppData/Roaming/QGIS/QGIS3/profiles/default/python/plugins/plugin_qgis_lpo"
- }
- ]
- },
- {
- "name": "Debug Tests",
- "type": "python",
- "request": "test",
- "console": "integratedTerminal",
- "justMyCode": false,
- "env": {
- "PYTEST_ADDOPTS": "--no-cov"
- }
- }
- ],
- }
-}
diff --git a/plugin_qgis_lpo/.gitattributes b/plugin_qgis_lpo/.gitattributes
deleted file mode 100644
index 9dad0d8..0000000
--- a/plugin_qgis_lpo/.gitattributes
+++ /dev/null
@@ -1,3 +0,0 @@
-.gitattributes export-ignore
-.editorconfig export-ignore
-test export-ignore
diff --git a/plugin_qgis_lpo/__about__.py b/plugin_qgis_lpo/__about__.py
new file mode 100644
index 0000000..03faf18
--- /dev/null
+++ b/plugin_qgis_lpo/__about__.py
@@ -0,0 +1,114 @@
+#! python3 # noqa: E265
+
+"""
+ Metadata about the package to easily retrieve informations about it.
+ See: https://packaging.python.org/guides/single-sourcing-package-version/
+"""
+
+# ############################################################################
+# ########## Libraries #############
+# ##################################
+
+# standard library
+from configparser import ConfigParser
+from datetime import date
+from pathlib import Path
+
+# ############################################################################
+# ########## Globals ###############
+# ##################################
+__all__: list = [
+ "__author__",
+ "__copyright__",
+ "__email__",
+ "__license__",
+ "__summary__",
+ "__title__",
+ "__uri__",
+ "__version__",
+]
+
+
+DIR_PLUGIN_ROOT: Path = Path(__file__).parent
+PLG_METADATA_FILE: Path = DIR_PLUGIN_ROOT.resolve() / "metadata.txt"
+
+
+# ############################################################################
+# ########## Functions #############
+# ##################################
+def plugin_metadata_as_dict() -> dict:
+ """Read plugin metadata.txt and returns it as a Python dict.
+
+ Raises:
+ IOError: if metadata.txt is not found
+
+ Returns:
+ dict: dict of dicts.
+ """
+ config = ConfigParser()
+ if PLG_METADATA_FILE.is_file():
+ config.read(PLG_METADATA_FILE.resolve(), encoding="UTF-8")
+ return {s: dict(config.items(s)) for s in config.sections()}
+ else:
+ raise IOError("Plugin metadata.txt not found at: %s" % PLG_METADATA_FILE)
+
+
+# ############################################################################
+# ########## Variables #############
+# ##################################
+
+# store full metadata.txt as dict into a var
+__plugin_md__: dict = plugin_metadata_as_dict()
+
+__author__: str = __plugin_md__.get("general").get("author")
+__copyright__: str = "2024 - {0}, {1}".format(date.today().year, __author__)
+__email__: str = __plugin_md__.get("general").get("email")
+__icon_path__: Path = DIR_PLUGIN_ROOT.resolve() / __plugin_md__.get("general").get(
+ "icon"
+)
+__icon_dir_path__: Path = DIR_PLUGIN_ROOT.resolve() / "resources" / "images"
+__keywords__: list = [
+ t.strip() for t in __plugin_md__.get("general").get("repository").split("tags")
+]
+__license__: str = "GPLv3"
+__summary__: str = "{}\n{}".format(
+ __plugin_md__.get("general").get("description"),
+ __plugin_md__.get("general").get("about"),
+)
+
+__title__: str = __plugin_md__.get("general").get("name")
+__title_clean__: str = "".join(e for e in __title__ if e.isalnum())
+
+__uri_homepage__: str = __plugin_md__.get("general").get("homepage")
+__uri_repository__: str = __plugin_md__.get("general").get("repository")
+__uri_tracker__: str = __plugin_md__.get("general").get("tracker")
+__uri__: str = __uri_repository__
+
+__version__: str = __plugin_md__.get("general").get("version")
+__version_info__: tuple = tuple(
+ [
+ int(num) if num.isdigit() else num
+ for num in __version__.replace("-", ".", 1).split(".")
+ ]
+)
+
+# #############################################################################
+# ##### Main #######################
+# ##################################
+if __name__ == "__main__":
+ plugin_md = plugin_metadata_as_dict()
+ assert isinstance(plugin_md, dict)
+ assert plugin_md.get("general").get("name") == __title__
+ print(f"Plugin: {__title__}")
+ print(f"By: {__author__}")
+ print(f"Version: {__version__}")
+ print(f"Description: {__summary__}")
+ print(f"Icon: {__icon_path__}")
+ print(
+ "For: %s > QGIS > %s"
+ % (
+ plugin_md.get("general").get("qgisminimumversion"),
+ plugin_md.get("general").get("qgismaximumversion"),
+ )
+ )
+ print(__title_clean__)
diff --git a/plugin_qgis_lpo/__init__.py b/plugin_qgis_lpo/__init__.py
index a79ddaf..72f1597 100644
--- a/plugin_qgis_lpo/__init__.py
+++ b/plugin_qgis_lpo/__init__.py
@@ -1,19 +1,23 @@
-import os
+#! python3 # noqa: E265
+"""init Qgis LPO Plugin"""
+# ----------------------------------------------------------
+# Copyright (C) 2015 Martin Dobias
+# ----------------------------------------------------------
+# Licensed under the terms of GNU GPL 2
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+# --------------------------------------------------------------------
-from qgis.gui import QgisInterface
-# from plugin_qgis_lpo.qgis_plugin_tools.infrastructure.debugging import ( # noqa F401
-# setup_debugpy,
-# setup_ptvsd,
-# setup_pydevd,
-# )
+def classFactory(iface): # noqa N802
+ """Load the plugin class.
-# debugger = os.environ.get("QGIS_PLUGIN_USE_DEBUGGER", "").lower()
-# if debugger in {"debugpy", "ptvsd", "pydevd"}:
-# locals()["setup_" + debugger]()
+ :param iface: A QGIS interface instance.
+ :type iface: QgsInterface
+ """
+ from .plugin_main import QgisLpoPlugin
-
-def classFactory(iface: QgisInterface): # noqa N802
- from .plugin import Plugin
-
- return Plugin(iface)
+ return QgisLpoPlugin(iface)
diff --git a/plugin_qgis_lpo/action_scripts/csv_formatter.py b/plugin_qgis_lpo/action_scripts/csv_formatter.py
index eb52c72..04c4e40 100644
--- a/plugin_qgis_lpo/action_scripts/csv_formatter.py
+++ b/plugin_qgis_lpo/action_scripts/csv_formatter.py
@@ -1,25 +1,51 @@
#####################################################
-##### OBJECTIFS DU SCRIPT : #
-##### Créer un fichier excel #
-##### Le remplir de valeurs #
-##### Ajouter des conditions de mise en forme #
+# OBJECTIFS DU SCRIPT : #
+# Créer un fichier excel #
+# Le remplir de valeurs #
+# Ajouter des conditions de mise en forme #
#####################################################
#####################################################
-##### 1 - Import des librairies #
+# 1 - Import des librairies #
#####################################################
import os
-import re
import webbrowser
from typing import Dict
from openpyxl import Workbook
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side # , Color
-from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QLabel, QSizePolicy, QVBoxLayout
from qgis.core import QgsProject
from qgis.gui import QgsMessageBar
from qgis.PyQt.QtCore import NULL
+from qgis.PyQt.QtWidgets import (
+ QDialog,
+ QDialogButtonBox,
+ QLabel,
+ QSizePolicy,
+ QVBoxLayout,
+)
+
+LR_COLORS = {
+ "EX": "000000",
+ "EW": "3d1851",
+ "RE": "5b1a62",
+ "CR": "d20019",
+ "EN": "fabf00",
+ "VU": "ffed00",
+ "NT": "faf2c7",
+ "LC": "78b747",
+ "DD": "d4d4d4",
+}
+
+
+def wc_hex_is_light(color):
+ """Set font color depending on fill darkness"""
+ red = int(color[0:2], 16)
+ green = int(color[2:4], 16)
+ blue = int(color[4:6], 16)
+ brightness = ((red * 299) + (green * 587) + (blue * 114)) / 1000
+ return "000000" if brightness > 155 else "ffffff"
class SuccessDialog(QDialog):
@@ -41,18 +67,19 @@ def __init__(self):
#####################################################
-##### 2 - Gestion des données #
+# >> 2 - Gestion des données #
#####################################################
-## Création du fichier excel
+# >> Création du fichier excel
wb = Workbook()
-## Sélection de "feuille" active 'worksheet'
+# >> Sélection de "feuille" active 'worksheet'
ws = wb.active
-## Alimentation du fichier excel avec les données de la table attributaire
+# >> Alimentation du fichier excel avec les données de la table attributaire
# Récupération de la couche
layer_id = "[%@layer_id%]"
layer = QgsProject().instance().mapLayer(layer_id)
+
# Ajout de l'entête
fields = layer.fields()
fields_row = []
@@ -71,16 +98,15 @@ def __init__(self):
feature_row.append(feature[attribute])
# print(feature_row)
ws.append(feature_row)
-
#####################################################
-##### 3 - Mise en forme du fichier #
+# >> 3 - Mise en forme du fichier #
#####################################################
-##### 3.1 - Mise en italique des cases 'Nom scientifique'
+# >> 3.1 - Mise en italique des cases 'Nom scientifique'
-## Définition du style
+# >> Définition du style
italic_grey_font = Font(color="606060", italic=True)
-## Rechercher la colonne "Nom scientifique"
+# >> Rechercher la colonne "Nom scientifique"
for col in ws["1:1"]:
if col.value == "Nom scientifique":
# Si on trouve une colonne référente alors :
@@ -90,72 +116,38 @@ def __init__(self):
cell.font = italic_grey_font
cell.alignment = Alignment(horizontal="center")
-##### 3.2 - Couleur sur les cases de type statut
+# >> 3.2 - Couleur sur les cases de type statut
-## Définition du style
-blackFill = PatternFill(start_color="000000", end_color="000000", fill_type="solid")
-purpleFill = PatternFill(start_color="3d1851", end_color="3d1851", fill_type="solid")
-lpurpleFill = PatternFill(start_color="5b1a62", end_color="5b1a62", fill_type="solid")
-redFill = PatternFill(start_color="d20019", end_color="d20019", fill_type="solid")
-orangeFill = PatternFill(start_color="fabf00", end_color="fabf00", fill_type="solid")
-yellowFill = PatternFill(start_color="ffed00", end_color="ffed00", fill_type="solid")
-beigeFill = PatternFill(start_color="faf2c7", end_color="faf2c7", fill_type="solid")
-greenFill = PatternFill(start_color="78b747", end_color="78b747", fill_type="solid")
-grey2Fill = PatternFill(start_color="d4d4d4", end_color="d4d4d4", fill_type="solid")
-
-## Définition de la fonction d'application des couleurs selon le statut
+# >> Définition de la fonction d'application des couleurs selon le statut
def color_statut_style(x):
- for cell in ws[x]:
- if re.match("EX", cell.value):
- cell.fill = blackFill
- elif re.match("EW", cell.value):
- cell.fill = purpleFill
- elif re.match("RE", cell.value):
- cell.fill = lpurpleFill
- elif re.match("CR", cell.value):
- cell.fill = redFill
- elif re.match("EN", cell.value):
- cell.fill = orangeFill
- elif re.match("VU", cell.value):
- cell.fill = yellowFill
- elif re.match("NT", cell.value):
- cell.fill = beigeFill
- elif re.match("LC", cell.value):
- cell.fill = greenFill
- elif re.match("DD", cell.value):
- cell.fill = grey2Fill
-
-
-## Recherche des colonnes de type statut
+ """set RedList color style"""
+ for lr_cell in ws[x]:
+ if lr_cell.value in LR_COLORS.keys():
+ color = LR_COLORS[lr_cell.value]
+ lr_cell.fill = PatternFill(
+ start_color=color, end_color=color, fill_type="solid"
+ )
+ lr_cell.font = Font(color=wc_hex_is_light(color))
+
+
+# >> Recherche des colonnes de type statut
# Rechercher la colonne "LR France", "LR Rhône-Alpes", "LR Auvergne" # d'autres colonnes à prévoir
for col in ws["1:1"]:
- if col.value == "LR France":
- # Si on trouve une colonne référente alors :
- range_statut = col
- ref_statut = range_statut.column_letter + ":" + range_statut.column_letter
- color_statut_style(ref_statut)
- elif col.value == "LR Rhône-Alpes":
- # Si on trouve une colonne référente alors :
- range_statut = col
- ref_statut = range_statut.column_letter + ":" + range_statut.column_letter
- color_statut_style(ref_statut)
- elif col.value == "LR Auvergne":
+ if col.value.startswith("LR "):
# Si on trouve une colonne référente alors :
range_statut = col
ref_statut = range_statut.column_letter + ":" + range_statut.column_letter
color_statut_style(ref_statut)
-##### 3.3 - Style général des colonnes
+# >> 3.3 - Style général des colonnes
-## Mise en gras des noms de colonnes
-col_name_font = Font(bold=True, italic=False, vertAlign=None, color="ffffff", size=12)
-blueLPO = PatternFill(start_color="0076bd", end_color="0076bd", fill_type="solid")
+# >> Mise en gras des noms de colonnes
for cell in ws["1:1"]:
- cell.font = col_name_font
- cell.fill = blueLPO
+ cell.font = Font(bold=True, italic=False, vertAlign=None, color="ffffff", size=12)
+ cell.fill = PatternFill(start_color="0076bd", end_color="0076bd", fill_type="solid")
-## Mise en forme de la largeur des colonnes
+# >> Mise en forme de la largeur des colonnes
dims: Dict[str, int] = {}
for row in ws.rows:
for cell in row:
@@ -170,13 +162,13 @@ def color_statut_style(x):
ws.column_dimensions[col].width = value
-## Mise en forme des bordures du tableau
+# >> Mise en forme des bordures du tableau
# Définition d'une fonction qui parcourt les cellules et applique le style de bordure choisie
def set_border(ws, cell_range):
border = Border(bottom=Side(border_style="thin", color="0076bd"))
- for row in ws[cell_range]:
- for cell in row:
- cell.border = border
+ for line in ws[cell_range]:
+ for rcell in line:
+ rcell.border = border
# Obtenir les dimensions du tableau
@@ -185,7 +177,7 @@ def set_border(ws, cell_range):
set_border(ws, "A1:Z3275")
#####################################################
-##### 4 - Enregistrement du résultat final #
+# >> 4 - Enregistrement du résultat final #
#####################################################
# Sauvegarde du fichier
diff --git a/plugin_qgis_lpo/action_scripts/joke.py b/plugin_qgis_lpo/action_scripts/joke.py
index 3b930e9..f1f1576 100644
--- a/plugin_qgis_lpo/action_scripts/joke.py
+++ b/plugin_qgis_lpo/action_scripts/joke.py
@@ -1,7 +1,13 @@
"""Fake news"""
-from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QLabel, QSizePolicy, QVBoxLayout
from qgis.gui import QgsMessageBar
+from qgis.PyQt.QtWidgets import (
+ QDialog,
+ QDialogButtonBox,
+ QLabel,
+ QSizePolicy,
+ QVBoxLayout,
+)
class JokeDialog(QDialog):
diff --git a/plugin_qgis_lpo/build.py b/plugin_qgis_lpo/build.py
deleted file mode 100644
index 803a77e..0000000
--- a/plugin_qgis_lpo/build.py
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-import glob
-from typing import List
-
-from qgis_plugin_tools.infrastructure.plugin_maker import PluginMaker
-
-"""
-#################################################
-# Edit the following to match the plugin
-#################################################
-"""
-
-py_files = [
- fil
- for fil in glob.glob("**/*.py", recursive=True)
- if "test/" not in fil and "test\\" not in fil
-]
-locales = ["fi"]
-profile = "default"
-ui_files = list(glob.glob("**/*.ui", recursive=True))
-resources = list(glob.glob("**/*.qrc", recursive=True))
-extra_dirs = ["resources"]
-compiled_resources: List[str] = []
-
-PluginMaker(
- py_files=py_files,
- ui_files=ui_files,
- resources=resources,
- extra_dirs=extra_dirs,
- compiled_resources=compiled_resources,
- locales=locales,
- profile=profile,
-)
diff --git a/plugin_qgis_lpo/commons/helpers.py b/plugin_qgis_lpo/commons/helpers.py
index d41abcd..c9895e6 100644
--- a/plugin_qgis_lpo/commons/helpers.py
+++ b/plugin_qgis_lpo/commons/helpers.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
/***************************************************************************
ScriptsLPO : common_functions.py
@@ -17,16 +15,13 @@
***************************************************************************/
"""
-
from datetime import datetime
-from typing import Any, Dict, List, Optional
+from typing import Any, Dict, List, Optional, Tuple
-import matplotlib.pyplot as plt
-import processing
+from qgis import processing
from qgis.core import (
QgsField,
QgsFields,
- QgsMessageLog,
QgsProcessingAlgorithm,
QgsProcessingContext,
QgsProcessingException,
@@ -55,19 +50,21 @@ def check_layer_is_valid(feedback: QgsProcessingFeedback, layer: QgsVectorLayer)
"""
if not layer.isValid():
raise QgsProcessingException(
- """"La couche PostGIS chargée n'est pas valide !
+ """La couche PostGIS chargée n'est pas valide !
Checkez les logs de PostGIS pour visualiser les messages d'erreur.
- Pour cela, rendez-vous dans l'onglet "Vue > Panneaux > Journal des messages" de QGis, puis l'onglet "PostGIS"."""
+ Pour cela, rendez-vous dans l'onglet "Vue > Panneaux > Journal des messages"
+ de QGis, puis l'onglet "PostGIS"."""
)
else:
# iface.messageBar().pushMessage("Info", "La couche PostGIS demandée est valide, la requête SQL a été exécutée avec succès !", level=Qgis.Info, duration=10)
feedback.pushInfo(
- "La couche PostGIS demandée est valide, la requête SQL a été exécutée avec succès !"
+ "La couche PostGIS demandée est valide, "
+ "la requête SQL a été exécutée avec succès !"
)
return None
-def construct_sql_array_polygons(layer: QgsVectorLayer):
+def sql_array_polygons_builder(layer: QgsVectorLayer):
"""
Construct the sql array containing the input vector layer's features geometry.
"""
@@ -77,9 +74,10 @@ def construct_sql_array_polygons(layer: QgsVectorLayer):
crs = layer.sourceCrs().authid()
if crs.split(":")[0] != "EPSG":
raise QgsProcessingException(
- """Le SCR (système de coordonnées de référence) de votre couche zone d'étude n'est pas de type 'EPSG'.
- Veuillez choisir un SCR adéquat.
- NB : 'EPSG:2154' pour Lambert 93 !"""
+ """Le SCR (système de coordonnées de référence) de votre couche zone \
+d'étude n'est pas de type 'EPSG'.
+Veuillez choisir un SCR adéquat.
+NB : 'EPSG:2154' pour Lambert 93 !"""
)
else:
crs = crs.split(":")[1]
@@ -103,7 +101,7 @@ def construct_sql_array_polygons(layer: QgsVectorLayer):
return array_polygons
-def construct_queries_list(
+def sql_queries_list_builder(
table_name: str, main_query: str, pk_field: str = "id"
) -> List[str]:
"""Table create"""
@@ -115,21 +113,22 @@ def construct_queries_list(
return queries
-def construct_sql_taxons_filter(taxons_dict: Dict) -> Optional[str]:
+def sql_taxons_filter_builder(taxons_dict: Dict) -> Optional[str]:
"""
Construct the sql "where" clause with taxons filters.
"""
rank_filters = []
for key, value in taxons_dict.items():
if value:
- rank_filters.append(f"{key} in {str(tuple(value))}")
+ value_list = ",".join([f"'{v}'" for v in value])
+ rank_filters.append(f"{key} in ({value_list})")
if len(rank_filters) > 0:
taxons_where = f"({' or '.join(rank_filters)})"
return taxons_where
return None
-def construct_sql_source_filter(sources: List[str]) -> Optional[str]:
+def sql_source_filter_builder(sources: List[str]) -> Optional[str]:
"""
Construct the sql "where" clause with source filters.
"""
@@ -138,7 +137,7 @@ def construct_sql_source_filter(sources: List[str]) -> Optional[str]:
return None
-def construct_sql_geom_type_filter(geom_types: List[str]) -> Optional[str]:
+def sql_geom_type_filter_builder(geom_types: List[str]) -> Optional[str]:
"""
Construct the sql "where" clause with source filters.
"""
@@ -156,7 +155,7 @@ def construct_sql_geom_type_filter(geom_types: List[str]) -> Optional[str]:
return None
-def construct_sql_datetime_filter(
+def sql_datetime_filter_builder(
self: QgsProcessingAlgorithm,
period_type_filter: str,
timestamp: datetime,
@@ -192,7 +191,7 @@ def construct_sql_datetime_filter(
return datetime_where
-def construct_sql_select_data_per_time_interval(
+def sql_timeinterval_cols_builder( # noqa C901
self: QgsProcessingAlgorithm,
time_interval_param,
start_year_param: int,
@@ -201,34 +200,48 @@ def construct_sql_select_data_per_time_interval(
parameters: Dict,
context: QgsProcessingContext,
feedback: QgsProcessingFeedback,
-):
+) -> Tuple[str, List[str]]:
"""
Construct the sql "select" data according to a time interval and a period.
"""
- select_data = ""
+ select_data = []
x_var = []
+ count_param = (
+ "*" if aggregation_type_param == "Nombre de données" else "DISTINCT t.cd_ref"
+ )
if time_interval_param == "Par année":
- add_five_years = self.parameterAsEnums(parameters, self.ADD_FIVE_YEARS, context)
+ add_five_years = self.parameterAsEnums(
+ parameters, self.ADD_FIVE_YEARS, context # type: ignore
+ )
if len(add_five_years) > 0:
if (end_year_param - start_year_param + 1) % 5 != 0:
raise QgsProcessingException(
- "Veuillez renseigner une période en année qui soit divisible par 5 ! Exemple : 2011 - 2020."
+ "Veuillez renseigner une période en année qui soit "
+ "divisible par 5 ! Exemple : 2011 - 2020."
)
else:
counter = start_year_param
step_limit = start_year_param
while counter <= end_year_param:
- select_data += f""", COUNT({"*" if aggregation_type_param == 'Nombre de données' else "DISTINCT t.cd_ref"}) filter (WHERE date_an={counter}) AS \"{counter}\""""
+ select_data.append(
+ f"""COUNT({count_param}) filter (WHERE date_an={counter}) AS \"{counter}\" """
+ )
x_var.append(str(counter))
if counter == step_limit + 4:
- select_data += f""", COUNT({"*" if aggregation_type_param == 'Nombre de données' else "DISTINCT t.cd_ref"}) filter (WHERE date_an>={counter-4} and date_an<={counter}) AS \"{counter-4} - {counter}\""""
+ select_data.append(
+ f"""COUNT({count_param}) filter (WHERE date_an>={counter-4} and date_an<={counter}) AS \"{counter-4} - {counter}\" """
+ )
step_limit += 5
counter += 1
else:
for year in range(start_year_param, end_year_param + 1):
- select_data += f""", COUNT({"*" if aggregation_type_param == 'Nombre de données' else "DISTINCT t.cd_ref"}) filter (WHERE date_an={year}) AS \"{year}\""""
+ select_data.append(
+ f"""COUNT({count_param}) filter (WHERE date_an={year}) AS \"{year}\""""
+ )
x_var.append(str(year))
- select_data += f""", COUNT({"*" if aggregation_type_param == 'Nombre de données' else "DISTINCT t.cd_ref"}) filter (WHERE date_an>={start_year_param} and date_an<={end_year_param}) AS \"TOTAL\""""
+ select_data.append(
+ f"""COUNT({count_param}) filter (WHERE date_an>={start_year_param} and date_an<={end_year_param}) AS \"TOTAL\""""
+ )
else:
start_month = self.parameterAsEnum(parameters, self.START_MONTH, context)
end_month = self.parameterAsEnum(parameters, self.END_MONTH, context)
@@ -253,53 +266,69 @@ def construct_sql_select_data_per_time_interval(
)
else:
for month in range(start_month, end_month + 1):
- select_data += f""", COUNT({"*" if aggregation_type_param == 'Nombre de données' else "DISTINCT t.cd_ref"}) filter (WHERE to_char(date, 'YYYY-MM')='{start_year_param}-{months_numbers_variables[month]}') AS \"{self._months_names_variables[month]} {start_year_param}\""""
+ select_data.append(
+ f"""COUNT({count_param}) filter (WHERE to_char(date, 'YYYY-MM')='{start_year_param}-{months_numbers_variables[month]}') AS \"{self._months_names_variables[month]} {start_year_param}\""""
+ )
x_var.append(
- self._months_names_variables[month]
+ self._months_names_variables[month] # type: ignore
+ " "
+ str(start_year_param)
)
elif end_year_param == start_year_param + 1:
for month in range(start_month, 12):
- select_data += f""", COUNT({"*" if aggregation_type_param == 'Nombre de données' else "DISTINCT t.cd_ref"}) filter (WHERE to_char(date, 'YYYY-MM')='{start_year_param}-{months_numbers_variables[month]}') AS \"{self._months_names_variables[month]} {start_year_param}\""""
+ select_data.append(
+ f"""COUNT({count_param}) filter (WHERE to_char(date, 'YYYY-MM')='{start_year_param}-{months_numbers_variables[month]}') AS \"{self._months_names_variables[month]} {start_year_param}\""""
+ )
x_var.append(
- self._months_names_variables[month] + " " + str(start_year_param)
+ self._months_names_variables[month] + " " + str(start_year_param) # type: ignore
)
for month in range(0, end_month + 1):
- select_data += f""", COUNT({"*" if aggregation_type_param == 'Nombre de données' else "DISTINCT t.cd_ref"}) filter (WHERE to_char(date, 'YYYY-MM')='{end_year_param}-{months_numbers_variables[month]}') AS \"{self._months_names_variables[month]} {end_year_param}\""""
+ select_data.append(
+ f"""COUNT({count_param}) filter (WHERE to_char(date, 'YYYY-MM')='{end_year_param}-{months_numbers_variables[month]}') AS \"{self._months_names_variables[month]} {end_year_param}\""""
+ )
x_var.append(
self._months_names_variables[month] + " " + str(end_year_param)
)
else:
for month in range(start_month, 12):
- select_data += f""", COUNT({"*" if aggregation_type_param == 'Nombre de données' else "DISTINCT t.cd_ref"}) filter (WHERE to_char(date, 'YYYY-MM')='{start_year_param}-{months_numbers_variables[month]}') AS \"{self._months_names_variables[month]} {start_year_param}\""""
+ select_data.append(
+ f"""COUNT({count_param}) filter (WHERE to_char(date, 'YYYY-MM')='{start_year_param}-{months_numbers_variables[month]}') AS \"{self._months_names_variables[month]} {start_year_param}\""""
+ )
x_var.append(
self._months_names_variables[month] + " " + str(start_year_param)
)
for year in range(start_year_param + 1, end_year_param):
for month in range(0, 12):
- select_data += f""", COUNT({"*" if aggregation_type_param == 'Nombre de données' else "DISTINCT t.cd_ref"}) filter (WHERE to_char(date, 'YYYY-MM')='{year}-{months_numbers_variables[month]}') AS \"{self._months_names_variables[month]} {year}\""""
+ select_data.append(
+ f"""COUNT({count_param}) filter (WHERE to_char(date, 'YYYY-MM')='{year}-{months_numbers_variables[month]}') AS \"{self._months_names_variables[month]} {year}\""""
+ )
x_var.append(self._months_names_variables[month] + " " + str(year))
for month in range(0, end_month + 1):
- select_data += f""", COUNT({"*" if aggregation_type_param == 'Nombre de données' else "DISTINCT t.cd_ref"}) filter (WHERE to_char(date, 'YYYY-MM')='{end_year_param}-{months_numbers_variables[month]}') AS \"{self._months_names_variables[month]} {end_year_param}\""""
+ select_data.append(
+ f"""COUNT({count_param}) filter (WHERE to_char(date, 'YYYY-MM')='{end_year_param}-{months_numbers_variables[month]}') AS \"{self._months_names_variables[month]} {end_year_param}\""""
+ )
x_var.append(
self._months_names_variables[month] + " " + str(end_year_param)
)
- select_data += f""", COUNT({"*" if aggregation_type_param == 'Nombre de données' else "DISTINCT t.cd_ref"}) filter (WHERE to_char(date, 'YYYY-MM')>='{start_year_param}-{months_numbers_variables[start_month]}' and to_char(date, 'YYYY-MM')<='{end_year_param}-{months_numbers_variables[end_month]}') AS \"TOTAL\""""
- feedback.pushDebugInfo(select_data)
- return select_data, x_var
+ select_data.append(
+ f"""COUNT({count_param}) filter (WHERE to_char(date, 'YYYY-MM')>='{start_year_param}-{months_numbers_variables[start_month]}' and to_char(date, 'YYYY-MM')<='{end_year_param}-{months_numbers_variables[end_month]}') AS \"TOTAL\""""
+ )
+ final_select_data = ", ".join(select_data)
+ feedback.pushDebugInfo(final_select_data)
+ return final_select_data, x_var
def load_layer(context: QgsProcessingContext, layer: QgsVectorLayer):
"""
Load a layer in the current project.
"""
- root = context.project().layerTreeRoot()
- plugin_lpo_group = root.findGroup("Résultats plugin LPO")
- if not plugin_lpo_group:
- plugin_lpo_group = root.insertGroup(0, "Résultats plugin LPO")
- context.project().addMapLayer(layer, False)
- plugin_lpo_group.insertLayer(0, layer)
+ if context.project() is not None:
+ root = context.project().layerTreeRoot()
+ plugin_lpo_group = root.findGroup("Résultats plugin LPO")
+ if not plugin_lpo_group:
+ plugin_lpo_group = root.insertGroup(0, "Résultats plugin LPO")
+ context.project().addMapLayer(layer, False)
+ plugin_lpo_group.insertLayer(0, layer)
def execute_sql_queries(
diff --git a/test/__init__.py b/plugin_qgis_lpo/gui/__init__.py
similarity index 100%
rename from test/__init__.py
rename to plugin_qgis_lpo/gui/__init__.py
diff --git a/plugin_qgis_lpo/gui/dlg_settings.py b/plugin_qgis_lpo/gui/dlg_settings.py
new file mode 100644
index 0000000..7a5a510
--- /dev/null
+++ b/plugin_qgis_lpo/gui/dlg_settings.py
@@ -0,0 +1,166 @@
+#! python3 # noqa: E265
+
+"""
+ Plugin settings form integrated into QGIS 'Options' menu.
+"""
+
+# standard
+import platform
+from functools import partial
+from pathlib import Path
+from urllib.parse import quote
+
+# PyQGIS
+from qgis.core import Qgis, QgsApplication
+from qgis.gui import QgsOptionsPageWidget, QgsOptionsWidgetFactory
+from qgis.PyQt import uic
+from qgis.PyQt.Qt import QUrl
+from qgis.PyQt.QtGui import QDesktopServices, QIcon
+
+# project
+from plugin_qgis_lpo.__about__ import (
+ __icon_dir_path__,
+ __title__,
+ __uri_homepage__,
+ __uri_tracker__,
+ __version__,
+)
+from plugin_qgis_lpo.toolbelt import PlgLogger, PlgOptionsManager
+from plugin_qgis_lpo.toolbelt.preferences import PlgSettingsStructure
+
+# ############################################################################
+# ########## Globals ###############
+# ##################################
+
+FORM_CLASS, _ = uic.loadUiType(
+ Path(__file__).parent / "{}.ui".format(Path(__file__).stem)
+)
+
+
+# ############################################################################
+# ########## Classes ###############
+# ##################################
+
+
+class ConfigOptionsPage(FORM_CLASS, QgsOptionsPageWidget):
+ """Settings form embedded into QGIS 'options' menu."""
+
+ def __init__(self, parent):
+ super().__init__(parent)
+ self.log = PlgLogger().log
+ self.plg_settings = PlgOptionsManager()
+
+ # load UI and set objectName
+ self.setupUi(self)
+ self.setObjectName("mOptionsPage{}".format(__title__))
+
+ report_context_message = quote(
+ "> Reported from plugin settings\n\n"
+ f"- operating system: {platform.system()} "
+ f"{platform.release()}_{platform.version()}\n"
+ f"- QGIS: {Qgis.QGIS_VERSION}"
+ f"- plugin version: {__version__}\n"
+ )
+
+ # header
+ self.lbl_title.setText(f"{__title__} - Version {__version__}")
+
+ # customization
+ self.btn_help.setIcon(QIcon(QgsApplication.iconPath("mActionHelpContents.svg")))
+ self.btn_help.pressed.connect(
+ partial(QDesktopServices.openUrl, QUrl(__uri_homepage__))
+ )
+
+ self.btn_report.setIcon(
+ QIcon(QgsApplication.iconPath("console/iconSyntaxErrorConsole.svg"))
+ )
+
+ self.btn_report.pressed.connect(
+ partial(QDesktopServices.openUrl, QUrl(f"{__uri_tracker__}new/choose"))
+ )
+
+ self.btn_reset.setIcon(QIcon(QgsApplication.iconPath("mActionUndo.svg")))
+ self.btn_reset.pressed.connect(self.reset_settings)
+
+ # load previously saved settings
+ self.load_settings()
+
+ def apply(self):
+ """Called to permanently apply the settings shown in the options page (e.g. \
+ save them to QgsSettings objects). This is usually called when the options \
+ dialog is accepted."""
+ settings = self.plg_settings.get_plg_settings()
+
+ # misc
+ settings.debug_mode = self.opt_debug.isChecked()
+ settings.version = __version__
+
+ # dump new settings into QgsSettings
+ self.plg_settings.save_from_object(settings)
+
+ if __debug__:
+ self.log(
+ message="DEBUG - Settings successfully saved.",
+ log_level=4,
+ )
+
+ def load_settings(self):
+ """Load options from QgsSettings into UI form."""
+ settings = self.plg_settings.get_plg_settings()
+
+ # global
+ self.opt_debug.setChecked(settings.debug_mode)
+ self.lbl_version_saved_value.setText(settings.version)
+
+ def reset_settings(self):
+ """Reset settings to default values (set in preferences.py module)."""
+ default_settings = PlgSettingsStructure()
+
+ # dump default settings into QgsSettings
+ self.plg_settings.save_from_object(default_settings)
+
+ # update the form
+ self.load_settings()
+
+
+class PlgOptionsFactory(QgsOptionsWidgetFactory):
+ """Factory for options widget."""
+
+ # def __init__(self):
+ # """Constructor."""
+ # super().__init__()
+
+ def icon(self) -> QIcon:
+ """Returns plugin icon, used to as tab icon in QGIS options tab widget.
+
+ :return: _description_
+ :rtype: QIcon
+ """
+ return QIcon(str(__icon_dir_path__ / "logo_lpo_aura_carre.png"))
+
+ def createWidget(self, parent) -> ConfigOptionsPage: # noqa N802
+ """Create settings widget.
+
+ :param parent: Qt parent where to include the options page.
+ :type parent: QObject
+
+ :return: options page for tab widget
+ :rtype: ConfigOptionsPage
+ """
+ return ConfigOptionsPage(parent)
+
+ def title(self) -> str:
+ """Returns plugin title, used to name the tab in QGIS options tab widget.
+
+ :return: plugin title from about module
+ :rtype: str
+ """
+ return __title__
+
+ def helpId(self) -> str:
+ """Returns plugin help URL.
+
+ :return: plugin homepage url from about module
+ :rtype: str
+ """
+ return __uri_homepage__
diff --git a/plugin_qgis_lpo/gui/dlg_settings.ui b/plugin_qgis_lpo/gui/dlg_settings.ui
new file mode 100644
index 0000000..b96a023
--- /dev/null
+++ b/plugin_qgis_lpo/gui/dlg_settings.ui
@@ -0,0 +1,245 @@
+
+
+ wdg_plugin_qgis_lpo_settings
+
+
+
+ 0
+ 0
+ 538
+ 273
+
+
+
+ Traitement des données LPO - Settings
+
+
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 30
+
+
+
+
+ 75
+ true
+
+
+
+
+
+
+ <html><head/><body><p align="center"><span style=" font-weight:600;">PluginTitle - Version X.X.X</span></p></body></html>
+
+
+ true
+
+
+ Qt::AlignCenter
+
+
+ true
+
+
+ false
+
+
+ Qt::TextSelectableByMouse
+
+
+
+ -
+
+
+
+ 0
+ 100
+
+
+
+
+
+
+ Miscellaneous
+
+
+ false
+
+
+
-
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 30
+
+
+
+
+
+
+ X.X.X
+
+
+ Qt::NoTextInteraction
+
+
+
+ -
+
+
+
+ 200
+ 25
+
+
+
+
+ 500
+ 30
+
+
+
+
+
+
+ Report an issue
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 30
+
+
+
+
+
+
+ Version used to save settings:
+
+
+
+ -
+
+
+
+ 200
+ 25
+
+
+
+
+ 500
+ 30
+
+
+
+
+
+
+ Help
+
+
+
+ -
+
+
+
+ 200
+ 25
+
+
+
+
+ 16777215
+ 30
+
+
+
+ true
+
+
+ Reset setttings to factory defaults
+
+
+
+ -
+
+
+
+ 0
+ 25
+
+
+
+
+ 16777215
+ 30
+
+
+
+ Enable debug mode.
+
+
+ true
+
+
+
+
+
+ Debug mode (degraded performances)
+
+
+ false
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 56
+
+
+
+
+
+
+
+
+
diff --git a/plugin_qgis_lpo/metadata.txt b/plugin_qgis_lpo/metadata.txt
index 040c432..3f8b5c1 100644
--- a/plugin_qgis_lpo/metadata.txt
+++ b/plugin_qgis_lpo/metadata.txt
@@ -1,19 +1,25 @@
[general]
-name=Traitements de la LPO AuRA
-qgisMinimumVersion=3.16
-description=Scripts de la LPO pour l'analyse de données
-about=Scripts développés par la LPO pour analyser les données naturalistes contenues dans une base Géonature, alimentée notamment par Visionature.
-version=3.0.0-dev
-author=collectif (LPO AuRA)
+name=Traitement des données LPO
+about=This plugin is a revolution!
+category=Database
+hasProcessingProvider=True
+description=Extends QGIS with revolutionary features that every single GIS end-users was expected (or not)!
+icon=resources/images/logo_lpo_aura_carre.png
+tags=geonature,visionature,faune-france,postgresql,lpo
+
+# credits and contact
+author=Pole VDC (LPOAuRA)
email=webadmin.aura@lpo.fr
+homepage=https://github.com/lpoaura/PluginQGis-LPOData
repository=https://github.com/lpoaura/PluginQGis-LPOData
tracker=https://github.com/lpoaura/PluginQGis-LPOData/issues
-hasProcessingProvider=yes
-category=Analysis
-changelog=https://github.com/lpoaura/PluginQGis-LPOData/blob/master/CHANGELOG.md
-experimental=True
+
+# experimental flag
deprecated=False
-tags=LPO AuRA, données, analyse, traitements, Python, PostGIS
-homepage=https://github.com/lpoaura/PluginQGis-LPOData
-icon=icons/logo_lpo_aura_carre.png
-server=False
+experimental=True
+qgisMinimumVersion=3.16
+qgisMaximumVersion=3.99
+
+# versioning
+version=3.0.0+dev
+changelog=
diff --git a/plugin_qgis_lpo/plugin.py b/plugin_qgis_lpo/plugin.py
deleted file mode 100644
index ecbab59..0000000
--- a/plugin_qgis_lpo/plugin.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-/***************************************************************************
- ScriptsLPO : scripts_lpo.py
- -------------------
-
- ***************************************************************************/
-
-/***************************************************************************
- * *
- * This program is free software; you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation; either version 2 of the License, or *
- * (at your option) any later version. *
- * *
- ***************************************************************************/
-"""
-
-
-from qgis.core import QgsApplication
-from qgis.PyQt.QtWidgets import QAction, QMenu
-
-from .processing.provider import Provider
-from .processing.qgis_processing_postgis import get_connection_name
-from .processing.species_map import CarteParEspece
-
-# from plugin_qgis_lpo.processing.provider import Provider
-# from plugin_qgis_lpo.qgis_plugin_tools.tools.custom_logging import (
-# setup_logger,
-# teardown_logger,
-# )
-# from plugin_qgis_lpo.qgis_plugin_tools.tools.resources import plugin_name
-
-
-# cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0]
-
-# if cmd_folder not in sys.path:
-# sys.path.insert(0, cmd_folder)
-
-
-class Plugin(object):
- """QGIS Plugin Implementation."""
-
- # name = plugin_name()
-
- def __init__(self, iface) -> None:
- # setup_logger(Plugin.name)
- # self.provider = None
- self.provider = Provider()
- self.iface = iface
- # def initProcessing(self):
- """Init Processing provider for QGIS >= 3.8."""
- # self.provider = Provider()
- # QgsApplication.processingRegistry().addProvider(self.provider)
-
- def initGui(self): # noqa N802
- # self.initProcessing()
- QgsApplication.processingRegistry().addProvider(self.provider)
-
- self.especes_action = QAction(
- # QIcon(),
- "Carte par espèces",
- self.iface.mainWindow(),
- )
- self.especes_action.triggered.connect(self.runEspeces)
- try:
- # Try to put the button in the LPO menu bar
- lpo_menu = [
- a
- for a in self.iface.pluginMenu().parent().findChildren(QMenu)
- if a.title() == "Plugin LPO"
- ][0]
- lpo_menu.addAction(self.especes_action)
- except IndexError:
- # If not successful put the button in the Plugins toolbar
- self.iface.addToolBarIcon(self.especes_action)
- self.iface.messageBar().pushWarning(
- "Attention",
- "La carte par espèces est accessible via la barre d'outils d'Extensions",
- )
-
- def runEspeces(self): # noqa N802
- connection_name = get_connection_name()
- if connection_name is not None:
- CarteParEspece(connection_name).exec()
-
- def unload(self):
- QgsApplication.processingRegistry().removeProvider(self.provider)
-
- try:
- lpo_menu = [
- a
- for a in self.iface.pluginMenu().parent().findChildren(QMenu)
- if a.title() == "Plugin LPO"
- ][0]
- lpo_menu.removeAction(self.especes_action)
- except IndexError:
- pass
- # teardown_logger(Plugin.name)
- self.iface.removeToolBarIcon(self.especes_action)
diff --git a/plugin_qgis_lpo/plugin_main.py b/plugin_qgis_lpo/plugin_main.py
new file mode 100644
index 0000000..5c3841c
--- /dev/null
+++ b/plugin_qgis_lpo/plugin_main.py
@@ -0,0 +1,209 @@
+#! python3 # noqa: E265
+
+"""
+ Main plugin module.
+"""
+
+# standard
+from functools import partial
+from pathlib import Path
+
+# PyQGIS
+from qgis.core import QgsApplication, QgsSettings
+from qgis.gui import QgisInterface
+from qgis.PyQt.QtCore import QCoreApplication, QLocale, QTranslator, QUrl
+from qgis.PyQt.QtGui import QDesktopServices, QIcon
+from qgis.PyQt.QtWidgets import QAction, QMenu
+
+# project
+from plugin_qgis_lpo.__about__ import (
+ DIR_PLUGIN_ROOT,
+ __icon_path__,
+ __title__,
+ __uri_homepage__,
+)
+from plugin_qgis_lpo.gui.dlg_settings import PlgOptionsFactory
+from plugin_qgis_lpo.processing.provider import QgisLpoProvider
+from plugin_qgis_lpo.processing.qgis_processing_postgis import get_connection_name
+from plugin_qgis_lpo.processing.species_map import CarteParEspece
+from plugin_qgis_lpo.toolbelt import PlgLogger
+
+# ############################################################################
+# ########## Classes ###############
+# ##################################
+
+
+class QgisLpoPlugin:
+ def __init__(self, iface: QgisInterface):
+ """Constructor.
+
+ :param iface: An interface instance that will be passed to this class which \
+ provides the hook by which you can manipulate the QGIS application at run time.
+ :type iface: QgsInterface
+ """
+ self.options_factory: PlgOptionsFactory
+ self.action_help: QAction
+ self.action_settings: QAction
+ self.action_help_plugin_menu_documentation: QAction
+ self.especes_action: QAction
+
+ self.provider = QgisLpoProvider()
+ self.iface = iface
+ self.log = PlgLogger().log
+
+ # translation
+ # initialize the locale
+ self.locale: str = QgsSettings().value("locale/userLocale", QLocale().name())[
+ 0:2
+ ]
+ locale_path: Path = (
+ DIR_PLUGIN_ROOT
+ / "resources"
+ / "i18n"
+ / f"{__title__.lower()}_{self.locale}.qm"
+ )
+ self.log(message=f"Translation: {self.locale}, {locale_path}", log_level=4)
+ if locale_path.exists():
+ self.translator = QTranslator()
+ self.translator.load(str(locale_path.resolve()))
+ QCoreApplication.installTranslator(self.translator)
+
+ def initGui(self):
+ """Set up plugin UI elements."""
+
+ # settings page within the QGIS preferences menu
+ self.options_factory = PlgOptionsFactory()
+ self.iface.registerOptionsWidgetFactory(self.options_factory)
+
+ # -- Actions
+ self.action_help = QAction(
+ QgsApplication.getThemeIcon("mActionHelpContents.svg"),
+ self.tr("Help"),
+ self.iface.mainWindow(),
+ )
+ self.action_help.triggered.connect(
+ partial(QDesktopServices.openUrl, QUrl(__uri_homepage__))
+ )
+
+ self.action_settings = QAction(
+ QgsApplication.getThemeIcon("console/iconSettingsConsole.svg"),
+ self.tr("Settings"),
+ self.iface.mainWindow(),
+ )
+ self.action_settings.triggered.connect(
+ lambda: self.iface.showOptionsDialog(
+ currentPage="mOptionsPage{}".format(__title__)
+ )
+ )
+
+ # -- Menu
+ self.iface.addPluginToMenu(__title__, self.action_settings)
+ self.iface.addPluginToMenu(__title__, self.action_help)
+
+ # -- Help menu
+
+ # documentation
+ self.iface.pluginHelpMenu().addSeparator()
+ self.action_help_plugin_menu_documentation = QAction(
+ QIcon(str(__icon_path__)),
+ f"{__title__} - Documentation",
+ self.iface.mainWindow(),
+ )
+ self.action_help_plugin_menu_documentation.triggered.connect(
+ partial(QDesktopServices.openUrl, QUrl(__uri_homepage__))
+ )
+
+ self.iface.pluginHelpMenu().addAction(
+ self.action_help_plugin_menu_documentation
+ )
+
+ QgsApplication.processingRegistry().addProvider(self.provider)
+
+ self.especes_action = QAction(
+ # QIcon(),
+ "Carte par espèces",
+ self.iface.mainWindow(),
+ )
+ self.especes_action.triggered.connect(self.runEspeces)
+ try:
+ # Try to put the button in the LPO menu bar
+ lpo_menu = [
+ a
+ for a in self.iface.pluginMenu().parent().findChildren(QMenu)
+ if a.title() == "Plugin LPO"
+ ][0]
+ lpo_menu.addAction(self.especes_action)
+ except IndexError:
+ # If not successful put the button in the Plugins toolbar
+ self.iface.addToolBarIcon(self.especes_action)
+ self.iface.messageBar().pushWarning(
+ "Attention",
+ "La carte par espèces est accessible via la barre d'outils d'Extensions",
+ )
+
+ def runEspeces(self): # noqa N802
+ connection_name = get_connection_name()
+ if connection_name is not None:
+ CarteParEspece(connection_name).exec()
+
+ def tr(self, message: str) -> str:
+ """Get the translation for a string using Qt translation API.
+
+ :param message: string to be translated.
+ :type message: str
+
+ :returns: Translated version of message.
+ :rtype: str
+ """
+ return QCoreApplication.translate(self.__class__.__name__, message)
+
+ def unload(self):
+ """Cleans up when plugin is disabled/uninstalled."""
+ # -- Clean up menu
+ self.iface.removePluginMenu(__title__, self.action_help)
+ self.iface.removePluginMenu(__title__, self.action_settings)
+
+ # -- Clean up preferences panel in QGIS settings
+ self.iface.unregisterOptionsWidgetFactory(self.options_factory)
+
+ # remove from QGIS help/extensions menu
+ if self.action_help_plugin_menu_documentation:
+ self.iface.pluginHelpMenu().removeAction(
+ self.action_help_plugin_menu_documentation
+ )
+
+ # remove actions
+ del self.action_settings
+ del self.action_help
+
+ QgsApplication.processingRegistry().removeProvider(self.provider)
+
+ try:
+ lpo_menu = [
+ a
+ for a in self.iface.pluginMenu().parent().findChildren(QMenu)
+ if a.title() == "Plugin LPO"
+ ][0]
+ lpo_menu.removeAction(self.especes_action)
+ except IndexError:
+ pass
+ # teardown_logger(Plugin.name)
+ self.iface.removeToolBarIcon(self.especes_action)
+
+ def run(self):
+ """Main process.
+
+ :raises Exception: if there is no item in the feed
+ """
+ try:
+ self.log(
+ message=self.tr("Everything ran OK."),
+ log_level=3,
+ push=False,
+ )
+ except Exception as err:
+ self.log(
+ message=self.tr("Houston, we've got a problem: {}".format(err)),
+ log_level=2,
+ push=True,
+ )
diff --git a/plugin_qgis_lpo/processing/__init__.py b/plugin_qgis_lpo/processing/__init__.py
index e69de29..626e6bf 100644
--- a/plugin_qgis_lpo/processing/__init__.py
+++ b/plugin_qgis_lpo/processing/__init__.py
@@ -0,0 +1,2 @@
+#! python3 # noqa: E265
+from .provider import QgisLpoProvider # noqa: F401
diff --git a/plugin_qgis_lpo/processing/extract_data.py b/plugin_qgis_lpo/processing/extract_data.py
index 1c21d9d..e8fcd25 100644
--- a/plugin_qgis_lpo/processing/extract_data.py
+++ b/plugin_qgis_lpo/processing/extract_data.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
/***************************************************************************
ScriptsLPO : summary_map.py
@@ -16,7 +14,6 @@
* *
***************************************************************************/
"""
-from typing import Dict
from .processing_algorithm import BaseProcessingAlgorithm
@@ -31,9 +28,18 @@ def __init__(self) -> None:
self._output_name = self._display_name
self._group_id = "raw_data"
self._group = "Données brutes"
- self._short_description = """Besoin d'aide ? Vous pouvez vous référer au Wiki accessible sur ce lien : https://github.com/lpoaura/PluginQGis-LPOData/wiki.
- Cet algorithme vous permet d'extraire des données d'observation contenues dans la base de données LPO (couche PostGIS de type points) à partir d'une zone d'étude présente dans votre projet QGIS (couche de type polygones).
- IMPORTANT : Prenez le temps de lire attentivement les instructions pour chaque étape, et particulièrement les informations en rouge !"""
+ self._short_description = """Besoin d'aide ?
+ Vous pouvez vous référer au Wikiaccessible sur ce lien : https://github.com/lpoaura/PluginQGis-LPOData/wiki.
+
+Cet algorithme vous permet d'extraire des données d'observation contenues dans la
+base de données LPO (couche PostGIS de type points) à partir d'une zone d'étude
+présente dans votre projet QGIS (couche de type polygones).
+IMPORTANT : Prenez le temps de lire
+attentivement les instructions pour chaque étape, et particulièrement les
+informations en rouge
+!"""
self._icon = "extract_data.png"
self._short_help_string = ""
self._is_map_layer = True
diff --git a/plugin_qgis_lpo/processing/extract_data_observers.py b/plugin_qgis_lpo/processing/extract_data_observers.py
index 13a6883..a7f75f9 100644
--- a/plugin_qgis_lpo/processing/extract_data_observers.py
+++ b/plugin_qgis_lpo/processing/extract_data_observers.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
/***************************************************************************
ScriptsLPO : summary_map.py
@@ -16,7 +14,6 @@
* *
***************************************************************************/
"""
-from typing import Dict
from .processing_algorithm import BaseProcessingAlgorithm
diff --git a/plugin_qgis_lpo/processing/processing_algorithm.py b/plugin_qgis_lpo/processing/processing_algorithm.py
index 6780fd1..b234801 100644
--- a/plugin_qgis_lpo/processing/processing_algorithm.py
+++ b/plugin_qgis_lpo/processing/processing_algorithm.py
@@ -1,6 +1,5 @@
-# -*- coding: utf-8 -*-
-
"""Generic Qgis Processing Algorithm classes"""
+
import ast
import json
import os
@@ -31,21 +30,23 @@
from qgis.PyQt.QtGui import QIcon
from qgis.utils import iface
+from ..__about__ import __icon_dir_path__
from ..commons.helpers import (
check_layer_is_valid,
- construct_queries_list,
- construct_sql_array_polygons,
- construct_sql_datetime_filter,
- construct_sql_geom_type_filter,
- construct_sql_select_data_per_time_interval,
- construct_sql_source_filter,
- construct_sql_taxons_filter,
execute_sql_queries,
format_layer_export,
load_layer,
simplify_name,
+ sql_array_polygons_builder,
+ sql_datetime_filter_builder,
+ sql_geom_type_filter_builder,
+ sql_queries_list_builder,
+ sql_source_filter_builder,
+ sql_taxons_filter_builder,
+ sql_timeinterval_cols_builder,
)
from ..commons.widgets import DateTimeWidget
+from ..toolbelt.log_handler import PlgLogger
from .qgis_processing_postgis import uri_from_name
plugin_path = os.path.dirname(__file__)
@@ -91,8 +92,10 @@ class BaseProcessingAlgorithm(QgsProcessingAlgorithm):
TYPE_GEOM = "TYPE_GEOM"
def __init__(self) -> None:
+ """Init class and set default values"""
super().__init__()
+ self.log = PlgLogger().log
# Global settings
self._name = "myprocessingalgorithm"
self._display_name = "My Processing Algorithm"
@@ -170,7 +173,7 @@ def __init__(self) -> None:
self._output_name = "output"
self._study_area = None
self._format_name: str = "output"
- self._areas_type: List[str] = []
+ self._areas_type: str
self._ts = datetime.now()
self._array_polygons = None
self._taxons_filters: Dict[str, List[str]] = {}
@@ -187,9 +190,12 @@ def __init__(self) -> None:
self._group_by_species: str = ""
self._taxa_fields: Optional[str] = None
self._custom_fields: Optional[str] = None
- self._x_var: Optional[str] = None
+ self._x_var: Optional[List[str]] = None
self._lr_columns_db: List[str] = ["lr_r"]
self._lr_columns_with_alias: List[str] = ['lr_r as "LR Régionale"']
+ self._time_interval: str
+ self._start_year: int
+ self._end_year: int
def tr(self, string: str) -> str:
"""QgsProcessingAlgorithm translatable string with the self.tr() function."""
@@ -234,7 +240,7 @@ def group(self) -> str:
def icon(self) -> QIcon:
"""Icon script"""
- return QIcon(os.path.join(plugin_path, os.pardir, "icons", self._icon))
+ return QIcon(str(__icon_dir_path__ / self._icon))
def shortHelpString(self) -> str: # noqa N802
"""
@@ -252,7 +258,6 @@ def initAlgorithm(self, _config: None) -> None: # noqa N802
Here we define the inputs and output of the algorithm, along
with some other properties.
"""
- super().initAlgorithm(_config)
required_text = '(requis)'
optional_text = "(facultatif)"
@@ -262,7 +267,8 @@ def initAlgorithm(self, _config: None) -> None: # noqa N802
QgsProcessingParameterProviderConnection(
self.DATABASE,
self.tr(
- f"""BASE DE DONNÉES {required_text} : sélectionnez votre connexion à la base de données LPO"""
+ f"""BASE DE DONNÉES {required_text} :
+ sélectionnez votre connexion à la base de données LPO"""
),
"postgres",
defaultValue="geonature_lpo",
@@ -274,7 +280,9 @@ def initAlgorithm(self, _config: None) -> None: # noqa N802
QgsProcessingParameterFeatureSource(
self.STUDY_AREA,
self.tr(
- f"""ZONE D'ÉTUDE {required_text} : sélectionnez votre zone d'étude, à partir de laquelle seront extraits les résultats"""
+ f"""ZONE D'ÉTUDE {required_text} :
+ sélectionnez votre zone d'étude,
+ à partir de laquelle seront extraits les résultats"""
),
[QgsProcessing.TypeVectorPolygon],
)
@@ -374,7 +382,9 @@ def initAlgorithm(self, _config: None) -> None: # noqa N802
period_type = QgsProcessingParameterEnum(
self.PERIOD,
self.tr(
- f"""PÉRIODE {required_text} : sélectionnez une période pour filtrer vos données d'observations"""
+ f"""PÉRIODE {required_text} :
+ sélectionnez une période pour filtrer vos données
+ d'observations"""
),
self._period_variables,
allowMultiple=False,
@@ -406,12 +416,29 @@ def initAlgorithm(self, _config: None) -> None: # noqa N802
end_date.setMetadata({"widget_wrapper": {"class": DateTimeWidget}})
self.addParameter(end_date)
+ if self._return_geo_agg:
+ areas_types = QgsProcessingParameterEnum(
+ self.AREAS_TYPE,
+ self.tr(
+ f"""TYPE D'ENTITÉS GÉOGRAPHIQUES {required_text} :
+ Sélectionnez le type d'entités géographiques
+ qui vous intéresse"""
+ ),
+ self._areas_variables,
+ allowMultiple=False,
+ )
+ areas_types.setMetadata(
+ {"widget_wrapper": {"useCheckBoxes": True, "columns": 5}}
+ )
+ self.addParameter(areas_types)
+
# ## Taxons filters ##
self.addParameter(
QgsProcessingParameterEnum(
self.GROUPE_TAXO,
self.tr(
- f"""TAXONS {optional_text} : filtrer les données par groupes taxonomiques"""
+ f"""TAXONS {optional_text} :
+ filtrer les données par groupes taxonomiques"""
),
self._db_variables.value("groupe_taxo"),
allowMultiple=True,
@@ -439,7 +466,8 @@ def initAlgorithm(self, _config: None) -> None: # noqa N802
histogram_options = QgsProcessingParameterEnum(
self.HISTOGRAM_OPTIONS,
self.tr(
- f"""HISTOGRAMME {optional_text} : générer un histogramme à partir des résultats."""
+ f"""HISTOGRAMME {optional_text} :
+ générer un histogramme à partir des résultats."""
),
self._histogram_variables,
defaultValue="Pas d'histogramme",
@@ -453,7 +481,8 @@ def initAlgorithm(self, _config: None) -> None: # noqa N802
output_histogram = QgsProcessingParameterFileDestination(
self.OUTPUT_HISTOGRAM,
self.tr(
- """Emplacement de l'enregistrement du ficher (format image PNG) de l'histogramme"""
+ """Emplacement de l'enregistrement du ficher
+ (format image PNG) de l'histogramme"""
),
self.tr("image PNG (*.png)"),
# optional=True,
@@ -468,7 +497,9 @@ def initAlgorithm(self, _config: None) -> None: # noqa N802
QgsProcessingParameterString(
self.OUTPUT_NAME,
self.tr(
- f"""PARAMÉTRAGE DES RESULTATS EN SORTIE {optional_text} : personnalisez le nom de votre couche en base de données"""
+ f"""PARAMÉTRAGE DES RESULTATS EN SORTIE
+ {optional_text} : personnalisez le nom de votre couche
+ en base de données"""
),
self.tr(self._output_name),
)
@@ -547,21 +578,6 @@ def initAlgorithm(self, _config: None) -> None: # noqa N802
)
self.addParameter(extra_where)
- if self._return_geo_agg:
- areas_types = QgsProcessingParameterEnum(
- self.AREAS_TYPE,
- self.tr(
- """TYPE D'ENTITÉS GÉOGRAPHIQUES
- *3/ Sélectionnez le type d'entités géographiques qui vous intéresse"""
- ),
- self._areas_variables,
- allowMultiple=False,
- )
- areas_types.setMetadata(
- {"widget_wrapper": {"useCheckBoxes": True, "columns": 3}}
- )
- self.addParameter(areas_types)
-
def processAlgorithm( # noqa N802
self,
parameters: Dict[str, Any],
@@ -580,24 +596,25 @@ def processAlgorithm( # noqa N802
self._connection = self.parameterAsString(parameters, self.DATABASE, context)
self._add_table = self.parameterAsBool(parameters, self.ADD_TABLE, context)
self._study_area = self.parameterAsSource(parameters, self.STUDY_AREA, context)
- feedback.pushDebugInfo(
- str(
+ self.log(
+ message=str(
[
self._db_variables.value("source_data")[i]
for i in (
self.parameterAsEnums(parameters, self.SOURCE_DATA, context)
)
]
- )
+ ),
+ log_level=4,
)
- self._source_data_where = construct_sql_source_filter(
+ self._source_data_where = sql_source_filter_builder(
[
self._db_variables.value("source_data")[i]
for i in (self.parameterAsEnums(parameters, self.SOURCE_DATA, context))
]
)
- self._type_geom_where = construct_sql_geom_type_filter(
+ self._type_geom_where = sql_geom_type_filter_builder(
[
self._type_geom_variables[i]
for i in (self.parameterAsEnums(parameters, self.TYPE_GEOM, context))
@@ -665,18 +682,18 @@ def processAlgorithm( # noqa N802
# WHERE clauses builder
# TODO: Manage use case
# self._filters.append(
- # f"ST_Intersects(la.geom, ST_union({construct_sql_array_polygons(self._study_area)})"
+ # f"ST_Intersects(la.geom, ST_union({sql_array_polygons_builder(self._study_area)})"
# )
if self._study_area:
- self._array_polygons = construct_sql_array_polygons(self._study_area)
+ self._array_polygons = sql_array_polygons_builder(self._study_area)
if not self._is_data_extraction:
self._filters += ["is_present", "is_valid"]
- taxon_filters = construct_sql_taxons_filter(self._taxons_filters)
+ taxon_filters = sql_taxons_filter_builder(self._taxons_filters)
if taxon_filters:
self._filters.append(taxon_filters)
# Complete the "where" filter with the datetime filter
- time_filter = construct_sql_datetime_filter(
+ time_filter = sql_datetime_filter_builder(
self, self._period_type, self._ts, parameters, context
)
if time_filter:
@@ -712,22 +729,22 @@ def processAlgorithm( # noqa N802
# self._taxonomic_rank_db = self._taxonomic_ranks_db[taxonomic_rank_index]
if self._has_time_interval_form:
- time_interval = self._time_interval_variables[
+ self._time_interval = self._time_interval_variables[
self.parameterAsEnum(parameters, self.TIME_INTERVAL, context)
]
- feedback.pushDebugInfo(f"time_interval {time_interval}")
- start_year = self.parameterAsInt(parameters, self.START_YEAR, context)
- feedback.pushDebugInfo(f"start_year {start_year}")
- end_year = self.parameterAsInt(parameters, self.END_YEAR, context)
- feedback.pushDebugInfo(f"end_year {end_year}")
- if end_year < start_year:
+ self.log(message=f"time_interval {self._time_interval}", log_level=4)
+ self._start_year = self.parameterAsInt(parameters, self.START_YEAR, context)
+ self.log(message=f"start_year {self._start_year}", log_level=4)
+ self._end_year = self.parameterAsInt(parameters, self.END_YEAR, context)
+ self.log(message=f"end_year {self._end_year}", log_level=4)
+ if self._end_year < self._start_year:
raise QgsProcessingException(
"Veuillez renseigner une année de fin postérieure à l'année de début !"
)
taxonomic_rank = self._taxonomic_ranks_labels[
self.parameterAsEnum(parameters, self.TAXONOMIC_RANK, context)
]
- feedback.pushDebugInfo(f"taxonomic_rank {taxonomic_rank}")
+ self.log(message=f"taxonomic_rank {taxonomic_rank}", log_level=4)
aggregation_type = "Nombre de données"
self._group_by_species = (
"obs.cd_nom, obs.cd_ref, nom_rang, nom_sci, obs.nom_vern, "
@@ -737,19 +754,26 @@ def processAlgorithm( # noqa N802
(
self._custom_fields,
self._x_var,
- ) = construct_sql_select_data_per_time_interval(
+ ) = sql_timeinterval_cols_builder(
self,
- time_interval,
- start_year,
- end_year,
+ self._time_interval,
+ self._start_year,
+ self._end_year,
aggregation_type,
parameters,
context,
feedback,
)
# Select species info (optional)
- select_species_info = """/*source_id_sp, */obs.cd_nom, obs.cd_ref, nom_rang as "Rang", groupe_taxo AS "Groupe taxo",
- obs.nom_vern AS "Nom vernaculaire", nom_sci AS "Nom scientifique\""""
+ select_species_info = """
+ /*source_id_sp, */
+ obs.cd_nom,
+ obs.cd_ref,
+ nom_rang as "Rang",
+ groupe_taxo AS "Groupe taxo",
+ obs.nom_vern AS "Nom vernaculaire",
+ nom_sci AS "Nom scientifique\"
+ """
# Select taxonomic groups info (optional)
select_taxo_groups_info = 'groupe_taxo AS "Groupe taxo"'
self._taxa_fields = (
@@ -757,7 +781,7 @@ def processAlgorithm( # noqa N802
if taxonomic_rank == "Espèces"
else select_taxo_groups_info
)
- feedback.pushDebugInfo(self._taxa_fields)
+ self.log(message=self._taxa_fields, log_level=4)
lr_columns = self._db_variables.value("lr_columns")
if lr_columns:
@@ -789,20 +813,20 @@ def processAlgorithm( # noqa N802
lr_columns_fields="\n, ".join(self._lr_columns_db),
lr_columns_with_alias="\n, ".join(self._lr_columns_with_alias),
)
- feedback.pushDebugInfo(query)
+ self.log(message=query, log_level=4)
geom_field = "geom" if self._is_map_layer else None
if self._add_table:
# Define the name of the PostGIS summary table which will be created in the DB
table_name = simplify_name(self._format_name)
# Define the SQL queries
- queries = construct_queries_list(table_name, query)
+ queries = sql_queries_list_builder(table_name, query)
# Execute the SQL queries
execute_sql_queries(context, feedback, self._connection, queries)
# Format the URI
- self._uri.setDataSource(None, table_name, geom_field, "", self._primary_key)
+ self._uri.setDataSource(None, table_name, geom_field, "", self._primary_key) # type: ignore
else:
# Format the URI with the query
- self._uri.setDataSource("", f"({query})", geom_field, "", self._primary_key)
+ self._uri.setDataSource("", f"({query})", geom_field, "", self._primary_key) # type: ignore
self._layer = QgsVectorLayer(self._uri.uri(), self._format_name, "postgres")
check_layer_is_valid(feedback, self._layer)
diff --git a/plugin_qgis_lpo/processing/provider.py b/plugin_qgis_lpo/processing/provider.py
index 189f81e..a7d06d2 100644
--- a/plugin_qgis_lpo/processing/provider.py
+++ b/plugin_qgis_lpo/processing/provider.py
@@ -1,102 +1,104 @@
-# -*- coding: utf-8 -*-
+#! python3 # noqa: E265
"""
-/***************************************************************************
- ScriptsLPO : scripts_lpo_provider.py
- -------------------
-
- ***************************************************************************/
-
-/***************************************************************************
- * *
- * This program is free software; you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation; either version 2 of the License, or *
- * (at your option) any later version. *
- * *
- ***************************************************************************/
+ Processing provider module.
"""
-__author__ = "LPO AuRA"
-__date__ = "2020-2024"
-
-# This will get replaced with a git SHA1 when you do a git archive
-__revision__ = "$Format:%H$"
-
-import os
-
-from qgis.core import QgsMessageLog, QgsProcessingProvider
+# PyQGIS
+from qgis.core import QgsProcessingProvider
+from qgis.PyQt.QtCore import QCoreApplication
from qgis.PyQt.QtGui import QIcon
-from .extract_data import ExtractData
-from .extract_data_observers import ExtractDataObservers
-from .state_of_knowledge import StateOfKnowledge
-from .summary_map import SummaryMap
-from .summary_table_per_species import SummaryTablePerSpecies
-from .summary_table_per_time_interval import SummaryTablePerTimeInterval
-
-plugin_path = os.path.dirname(__file__)
-
-
-class Provider(QgsProcessingProvider):
- # def __init__(self) -> None:
- # """
- # Default constructor.
- # """
- # super().__init__(self)
+# project
+from plugin_qgis_lpo.__about__ import __icon_dir_path__, __title__, __version__
+from plugin_qgis_lpo.processing.extract_data import ExtractData
+from plugin_qgis_lpo.processing.extract_data_observers import ExtractDataObservers
+from plugin_qgis_lpo.processing.state_of_knowledge import StateOfKnowledge
+from plugin_qgis_lpo.processing.summary_map import SummaryMap
+from plugin_qgis_lpo.processing.summary_table_per_species import SummaryTablePerSpecies
+from plugin_qgis_lpo.processing.summary_table_per_time_interval import (
+ SummaryTablePerTimeInterval,
+)
+
+# ############################################################################
+# ########## Classes ###############
+# ##################################
+
+
+class QgisLpoProvider(QgsProcessingProvider):
+ """
+ Processing provider class.
+ """
+
+ def loadAlgorithms(self):
+ """Loads all algorithms belonging to this provider."""
+ algorithms = [
+ ExtractData(),
+ ExtractDataObservers(),
+ SummaryTablePerSpecies(),
+ SummaryTablePerTimeInterval(),
+ StateOfKnowledge(),
+ SummaryMap(),
+ ]
+ for alg in algorithms:
+ self.addAlgorithm(alg)
def id(self) -> str:
+ """Unique provider id, used for identifying it. This string should be unique, \
+ short, character only string, eg "qgis" or "gdal". \
+ This string should not be localised.
+
+ :return: provider ID
+ :rtype: str
"""
- Returns the unique provider id, used for identifying the provider. This
- string should be a unique, short, character only string, eg "qgis" or
- "gdal". This string should not be localised.
- """
- return "lpoScripts"
+ return "plugin_qgis_lpo"
def name(self) -> str:
- """
- Returns the provider name, which is used to describe the provider
- within the GUI.
+ """Returns the provider name, which is used to describe the provider
+ within the GUI. This string should be short (e.g. "Lastools") and localised.
- This string should be short (e.g. "Lastools") and localised.
+ :return: provider name
+ :rtype: str
"""
- return self.tr("Traitements de la LPO")
+ return __title__
- def icon(self) -> "QIcon":
- """
- Should return a QIcon which is used for your provider inside
- the Processing toolbox.
+ def longName(self) -> str:
+ """Longer version of the provider name, which can include
+ extra details such as version numbers. E.g. "Lastools LIDAR tools".
+ This string should be localised. The default
+ implementation returns the same string as name().
+
+ :return: provider long name
+ :rtype: str
"""
- return QIcon(
- os.path.join(plugin_path, os.pardir, "icons", "logo_lpo_aura_carre.png")
- )
+ return self.tr("{} - Tools".format(__title__))
- # def unload(self) -> None:
- # """
- # Unloads the provider. Any tear-down steps required by the provider
- # should be implemented here.
- # """
+ def icon(self) -> QIcon:
+ """QIcon used for your provider inside the Processing toolbox menu.
- def loadAlgorithms(self): # noqa N802
+ :return: provider icon
+ :rtype: QIcon
"""
- Loads all algorithms belonging to this provider.
- """
- algorithms = [
- ExtractData(),
- ExtractDataObservers(),
- SummaryTablePerSpecies(),
- SummaryTablePerTimeInterval(),
- StateOfKnowledge(),
- SummaryMap(),
- ]
- for alg in algorithms:
- self.addAlgorithm(alg)
+ return QIcon(str(__icon_dir_path__ / "logo_lpo_aura_carre.png"))
+
+ def tr(self, message: str) -> str:
+ """Get the translation for a string using Qt translation API.
- def longName(self): # noqa N802
+ :param message: String for translation.
+ :type message: str, QString
+
+ :returns: Translated version of message.
+ :rtype: str
"""
- Returns the a longer version of the provider name, which can include
- extra details such as version numbers. E.g. "Lastools LIDAR tools
- (version 2.2.1)". This string should be localised. The default
- implementation returns the same string as name().
+ # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
+ return QCoreApplication.translate(self.__class__.__name__, message)
+
+ def versionInfo(self) -> str:
+ """Version information for the provider, or an empty string if this is not \
+ applicable (e.g. for inbuilt Processing providers). For plugin based providers, \
+ this should return the plugin’s version identifier.
+
+ :return: version
+ :rtype: str
"""
- return self.tr("Scripts d'exploitation de la base de donnée de la LPO")
+ return __version__
diff --git a/plugin_qgis_lpo/processing/qgis_processing_postgis.py b/plugin_qgis_lpo/processing/qgis_processing_postgis.py
index 91f0c1c..b9920c1 100644
--- a/plugin_qgis_lpo/processing/qgis_processing_postgis.py
+++ b/plugin_qgis_lpo/processing/qgis_processing_postgis.py
@@ -21,7 +21,7 @@
import os
import re
-from typing import List
+from typing import Optional
import psycopg2
import psycopg2.extensions # For isolation levels
@@ -109,7 +109,6 @@ def __init__(self, row):
class TableConstraint(object):
-
"""Class that represents a constraint of a table (relation)."""
(TypeCheck, TypeForeignKey, TypePrimaryKey, TypeUnique) = list(range(4))
@@ -617,21 +616,21 @@ def delete_table(self, table: str, schema: str = None):
"""Delete table from the database."""
table_name = self._table_name(schema, table)
- sql = "DROP TABLE %s" % table_name
+ sql = f"DROP TABLE {table_name}"
self._exec_sql_and_commit(sql)
def empty_table(self, table: str, schema: str = None):
"""Delete all rows from table."""
table_name = self._table_name(schema, table)
- sql = "DELETE FROM %s" % table_name
+ sql = f"DELETE FROM {table_name}"
self._exec_sql_and_commit(sql)
- def rename_table(self, table: str, new_table: str, schema: str = None):
+ def rename_table(self, table: str, new_table: str, schema: Optional[str] = None):
"""Rename a table in database."""
table_name = self._table_name(schema, table)
- sql = "ALTER TABLE %s RENAME TO %s" % (table_name, self._quote(new_table))
+ sql = f"ALTER TABLE {table_name} RENAME TO {self._quote(new_table)}"
self._exec_sql_and_commit(sql)
# Update geometry_columns if PostGIS is enabled
diff --git a/plugin_qgis_lpo/processing/species_map.py b/plugin_qgis_lpo/processing/species_map.py
index f8e0652..145afb8 100644
--- a/plugin_qgis_lpo/processing/species_map.py
+++ b/plugin_qgis_lpo/processing/species_map.py
@@ -2,13 +2,7 @@
import re
from pathlib import Path
-from qgis.core import (
- QgsDataSourceUri,
- QgsMessageLog,
- QgsProject,
- QgsProviderRegistry,
- QgsVectorLayer,
-)
+from qgis.core import QgsDataSourceUri, QgsProject, QgsProviderRegistry, QgsVectorLayer
from qgis.PyQt.QtCore import QEvent, QSortFilterProxyModel, Qt
from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel
from qgis.PyQt.QtWidgets import (
@@ -249,7 +243,12 @@ def accept(self) -> None:
with OverrideCursor(Qt.WaitCursor):
layer = QgsVectorLayer(uri.uri(), layer_name, "postgres")
layer.loadNamedStyle(
- str(Path(__file__).parent.parent / "styles" / "reproduction.qml")
+ str(
+ Path(__file__).parent.parent
+ / "resources"
+ / "styles"
+ / "reproduction.qml"
+ )
)
QgsProject.instance().addMapLayer(layer)
diff --git a/plugin_qgis_lpo/processing/state_of_knowledge.py b/plugin_qgis_lpo/processing/state_of_knowledge.py
index 964a75c..b5f00fb 100644
--- a/plugin_qgis_lpo/processing/state_of_knowledge.py
+++ b/plugin_qgis_lpo/processing/state_of_knowledge.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
/***************************************************************************
ScriptsLPO : state_of_knowledge.py
@@ -15,9 +13,6 @@
* *
***************************************************************************/
"""
-from typing import Dict
-
-from qgis.utils import iface
from .processing_algorithm import BaseProcessingAlgorithm
diff --git a/plugin_qgis_lpo/processing/summary_map.py b/plugin_qgis_lpo/processing/summary_map.py
index 31c10a1..f226dff 100644
--- a/plugin_qgis_lpo/processing/summary_map.py
+++ b/plugin_qgis_lpo/processing/summary_map.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
/***************************************************************************
ScriptsLPO : summary_map.py
@@ -16,7 +14,6 @@
* *
***************************************************************************/
"""
-from typing import Dict
from .processing_algorithm import BaseProcessingAlgorithm
@@ -33,9 +30,18 @@ def __init__(self) -> None:
self._output_name = self._display_name
self._group_id = "Map"
self._group = "Cartes"
- self._short_description = """Besoin d'aide ? Vous pouvez vous référer au Wiki accessible sur ce lien : https://github.com/lpoaura/PluginQGis-LPOData/wiki.
- Cet algorithme vous permet, à partir des données d'observation enregistrées dans la base de données LPO, de générer une carte de synthèse (couche PostGIS de type polygones) par maille ou par commune (au choix) basée sur une zone d'étude présente dans votre projet QGIS (couche de type polygones). Les données d'absence sont exclues de ce traitement.
- Pour chaque entité géographique, la table attributaire de la nouvelle couche fournit les informations suivantes :
+ self._short_description = """Besoin d'aide ?
+ Vous pouvez vous référer au Wiki accessible sur ce lien :
+
+ https://github.com/lpoaura/PluginQGis-LPOData/wiki.
+ Cet algorithme vous permet, à partir des données d'observation enregistrées
+ dans la base de données LPO, de générer une carte de synthèse
+ (couche PostGIS de type polygones) par maille ou par commune (au choix)
+ basée sur une zone d'étude présente dans votre projet QGIS (couche
+ de type polygones). Les données d'absence sont
+ exclues de ce traitement.
+ Pour chaque entité géographique, la table attributaire de la
+ nouvelle couche fournit les informations suivantes :
- Code de l'entité
- Surface (en km2)
- Nombre de données
@@ -45,27 +51,33 @@ def __init__(self) -> None:
- Nombre de dates
- Nombre de données de mortalité
- Liste des espèces observées
- Vous pouvez ensuite modifier la symbologie de la couche comme bon vous semble, en fonction du critère de votre choix.
- IMPORTANT : prenez le temps de lire attentivement les instructions pour chaque étape, et particulièrement les informations en rouge !"""
+ Vous pouvez ensuite modifier la symbologie de la couche comme bon
+ vous semble, en fonction du critère de votre choix.
+ IMPORTANT : prenez le temps de lire
+ attentivement les instructions pour chaque étape, et particulièrement
+ les informations en rouge
+ !"""
self._icon = "map.png"
self._short_help_string = ""
self._is_map_layer = True
+ self._return_geo_agg = True
self._query = """/*set random_page_cost to 4;*/
WITH prep AS (SELECT la.id_area, ((st_area(la.geom))::DECIMAL / 1000000) area_surface
FROM ref_geo.l_areas la
WHERE la.id_type = ref_geo.get_id_area_type('{areas_type}')
AND ST_intersects(la.geom, ST_union({array_polygons}))
),
- data AS (SELECT row_number() OVER () AS id,
- la.id_area,
- round(area_surface, 2) AS "Surface (km2)",
- count(*) AS "Nb de données",
- ROUND(COUNT(*) / ROUND(area_surface, 2), 2) AS "Densité (Nb de données/km2)",
- COUNT(DISTINCT cd_ref) FILTER (WHERE id_rang='ES') AS "Nb d'espèces",
- COUNT(DISTINCT observateur) AS "Nb d'observateurs",
- COUNT(DISTINCT DATE) AS "Nb de dates",
- COUNT(DISTINCT obs.id_synthese) FILTER (WHERE mortalite) AS "Nb de données de mortalité",
- string_agg(DISTINCT obs.nom_vern,', ') FILTER (WHERE id_rang='ES') AS "Liste des espèces observées"
+ data AS (SELECT
+ row_number() OVER () AS id,
+ la.id_area,
+ round(area_surface, 2) AS "Surface (km2)",
+ count(*) AS "Nb de données",
+ ROUND(COUNT(*) / ROUND(area_surface, 2), 2) AS "Densité (Nb de données/km2)",
+ COUNT(DISTINCT cd_ref) FILTER (WHERE id_rang='ES') AS "Nb d'espèces",
+ COUNT(DISTINCT observateur) AS "Nb d'observateurs",
+ COUNT(DISTINCT DATE) AS "Nb de dates",
+ COUNT(DISTINCT obs.id_synthese) FILTER (WHERE mortalite) AS "Nb de données de mortalité",
+ string_agg(DISTINCT obs.nom_vern,', ') FILTER (WHERE id_rang='ES') AS "Liste des espèces observées"
FROM prep la
LEFT JOIN gn_synthese.cor_area_synthese cor
ON la.id_area=cor.id_area
diff --git a/plugin_qgis_lpo/processing/summary_table_per_species.py b/plugin_qgis_lpo/processing/summary_table_per_species.py
index a708018..05b1285 100644
--- a/plugin_qgis_lpo/processing/summary_table_per_species.py
+++ b/plugin_qgis_lpo/processing/summary_table_per_species.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
/***************************************************************************
ScriptsLPO : summary_table_per_species.py
@@ -17,11 +15,6 @@
***************************************************************************/
"""
-
-from typing import Dict
-
-from qgis.utils import iface
-
from .processing_algorithm import BaseProcessingAlgorithm
@@ -82,99 +75,100 @@ def __init__(self) -> None:
self._icon = "table.png"
# self._short_description = ""
self._is_map_layer = False
- self._query = """WITH obs AS (
- /* selection des cd_nom */
- SELECT observations.*
- FROM src_lpodatas.v_c_observations_light observations
- WHERE ST_intersects(observations.geom, ST_union({array_polygons}))
- and {where_filters}),
- communes AS (
- /* selection des communes */
- SELECT DISTINCT obs.id_synthese, la.area_name
- FROM obs
- LEFT JOIN gn_synthese.cor_area_synthese cor ON obs.id_synthese = cor.id_synthese
- JOIN ref_geo.l_areas la ON cor.id_area = la.id_area
- WHERE la.id_type = ref_geo.get_id_area_type('COM')),
- atlas_code as (
- /* préparation codes atlas */
- SELECT cd_nomenclature, label_fr, hierarchy
- FROM ref_nomenclatures.t_nomenclatures
- WHERE id_type=(
- select ref_nomenclatures.get_id_nomenclature_type('VN_ATLAS_CODE')
- )
- ),
- total_count AS (
- /* comptage nb total individus */
- SELECT COUNT(*) AS total_count
- FROM obs),
- data AS (
- /* selection des données + statut */
- SELECT
- obs.cd_ref
- , obs.vn_id
- , r.nom_rang
- , groupe_taxo
- , string_agg(distinct obs.nom_vern, ', ') nom_vern
- , string_agg(distinct obs.nom_sci, ', ') nom_sci
- , COUNT(DISTINCT obs.id_synthese) AS nb_donnees
- , COUNT(DISTINCT obs.observateur) AS nb_observateurs
- , COUNT(DISTINCT obs.date) AS nb_dates
- , SUM(CASE WHEN mortalite THEN 1 ELSE 0 END) AS nb_mortalite
- , st.lr_france
- , {lr_columns_fields}
- , st.n2k
- , st.prot_nat as protection_nat
- , st.conv_berne
- , st.conv_bonn
- , max(ac.hierarchy) AS max_hierarchy_atlas_code
- , max(obs.nombre_total) AS nb_individus_max
- , min(obs.date_an) AS premiere_observation
- , max(obs.date_an) AS derniere_observation
- , string_agg(DISTINCT com.area_name, ', ') AS communes
- , string_agg(DISTINCT obs.source, ', ') AS sources
- FROM obs
- LEFT JOIN atlas_code ac ON obs.oiso_code_nidif = ac.cd_nomenclature::int
- LEFT JOIN taxonomie.bib_taxref_rangs r ON obs.id_rang = r.id_rang
- LEFT JOIN communes com ON obs.id_synthese = com.id_synthese
- left join taxonomie.mv_c_statut st on st.cd_ref=obs.cd_ref
- GROUP BY
- groupe_taxo
- , obs.cd_ref
- , obs.vn_id
- , r.nom_rang
- , st.lr_france
- , {lr_columns_fields}
- , st.n2k
- , st.prot_nat
- , st.conv_berne
- , st.conv_bonn),
- synthese AS (
- SELECT DISTINCT
- d.cd_ref
- , vn_id
- , nom_rang AS "Rang"
- , d.groupe_taxo AS "Groupe taxo"
- , nom_vern AS "Nom vernaculaire"
- , nom_sci AS "Nom scientifique"
- , nb_donnees AS "Nb de données"
- , ROUND(nb_donnees::DECIMAL / total_count, 4) * 100 AS "Nb données / nb données total (%)"
- , nb_observateurs AS "Nb d'observateurs"
- , nb_dates AS "Nb de dates"
- , nb_mortalite AS "Nb de données de mortalité"
- , lr_france AS "LR France"
- , {lr_columns_with_alias}
- , n2k AS "Natura 2000"
- , protection_nat AS "Protection nationale"
- , conv_berne AS "Convention de Berne"
- , conv_bonn AS "Convention de Bonn"
- , ac.label_fr AS "Statut nidif"
- , nb_individus_max AS "Nb d'individus max"
- , premiere_observation AS "Année première obs"
- , derniere_observation AS "Année dernière obs"
- , communes AS "Liste de communes"
- , sources AS "Sources"
- FROM total_count, data d
- LEFT JOIN atlas_code ac ON d.max_hierarchy_atlas_code = ac.hierarchy
- ORDER BY groupe_taxo,vn_id, nom_vern)
- SELECT row_number() OVER () AS id, *
- FROM synthese"""
+ self._query = """
+ WITH obs AS (
+ /* selection des cd_nom */
+ SELECT observations.*
+ FROM src_lpodatas.v_c_observations_light observations
+ WHERE ST_intersects(observations.geom, ST_union({array_polygons}))
+ and {where_filters}),
+ communes AS (
+ /* selection des communes */
+ SELECT DISTINCT obs.id_synthese, la.area_name
+ FROM obs
+ LEFT JOIN gn_synthese.cor_area_synthese cor ON obs.id_synthese = cor.id_synthese
+ JOIN ref_geo.l_areas la ON cor.id_area = la.id_area
+ WHERE la.id_type = ref_geo.get_id_area_type('COM')),
+ atlas_code as (
+ /* préparation codes atlas */
+ SELECT cd_nomenclature, label_fr, hierarchy
+ FROM ref_nomenclatures.t_nomenclatures
+ WHERE id_type=(
+ select ref_nomenclatures.get_id_nomenclature_type('VN_ATLAS_CODE')
+ )
+ ),
+ total_count AS (
+ /* comptage nb total individus */
+ SELECT COUNT(*) AS total_count
+ FROM obs),
+ data AS (
+ /* selection des données + statut */
+ SELECT
+ obs.cd_ref
+ , obs.vn_id
+ , r.nom_rang
+ , groupe_taxo
+ , string_agg(distinct obs.nom_vern, ', ') nom_vern
+ , string_agg(distinct obs.nom_sci, ', ') nom_sci
+ , COUNT(DISTINCT obs.id_synthese) AS nb_donnees
+ , COUNT(DISTINCT obs.observateur) AS nb_observateurs
+ , COUNT(DISTINCT obs.date) AS nb_dates
+ , SUM(CASE WHEN mortalite THEN 1 ELSE 0 END) AS nb_mortalite
+ , st.lr_france
+ , {lr_columns_fields}
+ , st.n2k
+ , st.prot_nat as protection_nat
+ , st.conv_berne
+ , st.conv_bonn
+ , max(ac.hierarchy) AS max_hierarchy_atlas_code
+ , max(obs.nombre_total) AS nb_individus_max
+ , min(obs.date_an) AS premiere_observation
+ , max(obs.date_an) AS derniere_observation
+ , string_agg(DISTINCT com.area_name, ', ') AS communes
+ , string_agg(DISTINCT obs.source, ', ') AS sources
+ FROM obs
+ LEFT JOIN atlas_code ac ON obs.oiso_code_nidif = ac.cd_nomenclature::int
+ LEFT JOIN taxonomie.bib_taxref_rangs r ON obs.id_rang = r.id_rang
+ LEFT JOIN communes com ON obs.id_synthese = com.id_synthese
+ left join taxonomie.mv_c_statut st on st.cd_ref=obs.cd_ref
+ GROUP BY
+ groupe_taxo
+ , obs.cd_ref
+ , obs.vn_id
+ , r.nom_rang
+ , st.lr_france
+ , {lr_columns_fields}
+ , st.n2k
+ , st.prot_nat
+ , st.conv_berne
+ , st.conv_bonn),
+ synthese AS (
+ SELECT DISTINCT
+ d.cd_ref
+ , array_to_string(vn_id,', ') as vn_id
+ , nom_rang AS "Rang"
+ , d.groupe_taxo AS "Groupe taxo"
+ , nom_vern AS "Nom vernaculaire"
+ , nom_sci AS "Nom scientifique"
+ , nb_donnees AS "Nb de données"
+ , ROUND(nb_donnees::DECIMAL / total_count, 4) * 100 AS "Nb données / nb données total (%)"
+ , nb_observateurs AS "Nb d'observateurs"
+ , nb_dates AS "Nb de dates"
+ , nb_mortalite AS "Nb de données de mortalité"
+ , lr_france AS "LR France"
+ , {lr_columns_with_alias}
+ , n2k AS "Natura 2000"
+ , protection_nat AS "Protection nationale"
+ , conv_berne AS "Convention de Berne"
+ , conv_bonn AS "Convention de Bonn"
+ , ac.label_fr AS "Statut nidif"
+ , nb_individus_max AS "Nb d'individus max"
+ , premiere_observation AS "Année première obs"
+ , derniere_observation AS "Année dernière obs"
+ , communes AS "Liste de communes"
+ , sources AS "Sources"
+ FROM total_count, data d
+ LEFT JOIN atlas_code ac ON d.max_hierarchy_atlas_code = ac.hierarchy
+ ORDER BY groupe_taxo,vn_id, nom_vern)
+ SELECT row_number() OVER () AS id, *
+ FROM synthese"""
diff --git a/plugin_qgis_lpo/processing/summary_table_per_species_lpoaura.py b/plugin_qgis_lpo/processing/summary_table_per_species_lpoaura.py.old
similarity index 95%
rename from plugin_qgis_lpo/processing/summary_table_per_species_lpoaura.py
rename to plugin_qgis_lpo/processing/summary_table_per_species_lpoaura.py.old
index e201653..ef9b436 100644
--- a/plugin_qgis_lpo/processing/summary_table_per_species_lpoaura.py
+++ b/plugin_qgis_lpo/processing/summary_table_per_species_lpoaura.py.old
@@ -37,10 +37,10 @@
from ..commons.helpers import (
check_layer_is_valid,
- construct_queries_list,
- construct_sql_array_polygons,
- construct_sql_datetime_filter,
- construct_sql_taxons_filter,
+ sql_queries_list_builder,
+ sql_array_polygons_builder,
+ sql_datetime_filter_builder,
+ sql_taxons_filter_builder,
execute_sql_queries,
load_layer,
simplify_name,
@@ -215,7 +215,7 @@ def processAlgorithm(self, parameters, context, feedback): # noqa N802
### CONSTRUCT "WHERE" CLAUSE (SQL) ###
# Construct the sql array containing the study area's features geometry
- array_polygons = construct_sql_array_polygons(study_area)
+ array_polygons = sql_array_polygons_builder(study_area)
# Define the "where" clause of the SQL query, aiming to retrieve the output PostGIS layer = summary table
where = f"""is_valid and is_present
and ST_intersects(obs.geom, ST_union({array_polygons}))"""
@@ -230,10 +230,10 @@ def processAlgorithm(self, parameters, context, feedback): # noqa N802
"obs.group1_inpn": group1_inpn,
"obs.group2_inpn": group2_inpn,
}
- taxons_where = construct_sql_taxons_filter(taxons_filters)
+ taxons_where = sql_taxons_filter_builder(taxons_filters)
where += taxons_where
# Complete the "where" clause with the datetime filter
- datetime_where = construct_sql_datetime_filter(
+ datetime_where = sql_datetime_filter_builder(
self, period_type, ts, parameters, context
)
where += datetime_where
@@ -350,7 +350,7 @@ def processAlgorithm(self, parameters, context, feedback): # noqa N802
# Define the name of the PostGIS summary table which will be created in the DB
table_name = simplify_name(format_name)
# Define the SQL queries
- queries = construct_queries_list(table_name, query)
+ queries = sql_queries_list_builder(table_name, query)
# Execute the SQL queries
execute_sql_queries(context, feedback, connection, queries)
# Format the URI
diff --git a/plugin_qgis_lpo/processing/summary_table_per_time_interval.py b/plugin_qgis_lpo/processing/summary_table_per_time_interval.py
index be7a378..ce989e6 100644
--- a/plugin_qgis_lpo/processing/summary_table_per_time_interval.py
+++ b/plugin_qgis_lpo/processing/summary_table_per_time_interval.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
"""
/***************************************************************************
ScriptsLPO : summary_table_per_species.py
@@ -17,12 +15,7 @@
***************************************************************************/
"""
-
-from typing import Dict
-
-from qgis.utils import iface
-
-from .processing_algorithm import BaseProcessingAlgorithm
+from plugin_qgis_lpo.processing.processing_algorithm import BaseProcessingAlgorithm
class SummaryTablePerTimeInterval(BaseProcessingAlgorithm):
@@ -66,7 +59,10 @@ def __init__(self) -> None:
"Pas d'histogramme",
"Total par pas de temps",
]
- self._query = """SELECT row_number() OVER () AS id, {taxa_fields}{custom_fields}
+ self._query = """SELECT
+ row_number() OVER () AS id,
+ {taxa_fields},
+ {custom_fields}
FROM src_lpodatas.v_c_observations_light obs
LEFT JOIN taxonomie.bib_taxref_rangs r ON obs.id_rang = r.id_rang
WHERE
diff --git a/plugin_qgis_lpo/processing/summary_table_per_time_interval_old.py b/plugin_qgis_lpo/processing/summary_table_per_time_interval_old.py
deleted file mode 100644
index e86a02d..0000000
--- a/plugin_qgis_lpo/processing/summary_table_per_time_interval_old.py
+++ /dev/null
@@ -1,717 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-/***************************************************************************
- ScriptsLPO : summary_table_per_time_interval.py
- -------------------
- ***************************************************************************/
-
-/***************************************************************************
- * *
- * This program is free software; you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation; either version 2 of the License, or *
- * (at your option) any later version. *
- * *
- ***************************************************************************/
-"""
-
-
-import os
-from datetime import datetime
-from typing import Dict
-
-import matplotlib.pyplot as plt
-from qgis.core import (
- QgsAction,
- QgsProcessing,
- QgsProcessingAlgorithm,
- QgsProcessingException,
- QgsProcessingOutputVectorLayer,
- QgsProcessingParameterBoolean,
- QgsProcessingParameterDefinition,
- QgsProcessingParameterEnum,
- QgsProcessingParameterFeatureSource,
- QgsProcessingParameterFileDestination,
- QgsProcessingParameterNumber,
- QgsProcessingParameterProviderConnection,
- QgsProcessingParameterString,
- QgsSettings,
- QgsVectorLayer,
-)
-from qgis.PyQt.QtCore import QCoreApplication
-from qgis.PyQt.QtGui import QIcon
-from qgis.utils import iface
-
-from ..commons.helpers import (
- check_layer_is_valid,
- construct_queries_list,
- construct_sql_array_polygons,
- construct_sql_select_data_per_time_interval,
- construct_sql_taxons_filter,
- execute_sql_queries,
- load_layer,
- simplify_name,
-)
-from .processing_algorithm import BaseProcessingAlgorithm
-
-# from processing.tools import postgis
-from .qgis_processing_postgis import uri_from_name
-
-plugin_path = os.path.dirname(__file__)
-
-
-class SummaryTablePerTimeIntervalOld(BaseProcessingAlgorithm):
- """
- This algorithm takes a connection to a data base and a vector polygons layer and
- returns a summary non geometric PostGIS layer.
- """
-
- # Constants used to refer to parameters and outputs
- DATABASE = "DATABASE"
- STUDY_AREA = "STUDY_AREA"
- TIME_INTERVAL = "TIME_INTERVAL"
- ADD_FIVE_YEARS = "ADD_FIVE_YEARS"
- TEST = "TEST"
- START_MONTH = "START_MONTH"
- START_YEAR = "START_YEAR"
- END_MONTH = "END_MONTH"
- END_YEAR = "END_YEAR"
- TAXONOMIC_RANK = "TAXONOMIC_RANK"
- AGG = "AGG"
- GROUPE_TAXO = "GROUPE_TAXO"
- REGNE = "REGNE"
- PHYLUM = "PHYLUM"
- CLASSE = "CLASSE"
- ORDRE = "ORDRE"
- FAMILLE = "FAMILLE"
- GROUP1_INPN = "GROUP1_INPN"
- GROUP2_INPN = "GROUP2_INPN"
- EXTRA_WHERE = "EXTRA_WHERE"
- OUTPUT = "OUTPUT"
- OUTPUT_NAME = "OUTPUT_NAME"
- ADD_TABLE = "ADD_TABLE"
- OUTPUT_HISTOGRAM = "OUTPUT_HISTOGRAM"
- ADD_HISTOGRAM = "ADD_HISTOGRAM"
-
- def name(self) -> str:
- return "SummaryTablePerTimeIntervalOld"
-
- def displayName(self): # noqa N802
- return "Tableau de synthèse par intervalle de temps (OLD)"
-
- def icon(self) -> "QIcon":
- return QIcon(os.path.join(plugin_path, os.pardir, "icons", "table.png"))
-
- def groupId(self): # noqa N802
- return "summary_tables"
-
- def group(self) -> str:
- return "Tableaux de synthèse"
-
- def shortDescription(self): # noqa N802
- return self.tr(
- """Besoin d'aide ? Vous pouvez vous référer au Wiki accessible sur ce lien : https://github.com/lpoaura/PluginQGis-LPOData/wiki.
- Cet algorithme vous permet, à partir des données d'observation enregistrées dans la base de données LPO, d'obtenir un tableau bilan (couche PostgreSQL)...
- - par année ou par mois (au choix)
- - et par espèce ou par groupe taxonomique (au choix)
- ... basé sur une zone d'étude présente dans votre projet QGis (couche de type polygones) et selon une période de votre choix.
- Les données d'absence sont exclues de ce traitement.
- IMPORTANT : Les étapes indispensables sont marquées d'une étoile * avant leur numéro. Prenez le temps de lire attentivement les instructions pour chaque étape, et particulièrement les informations en rouge !"""
- )
-
- def initAlgorithm(self, config=None): # noqa N802
- """
- Here we define the inputs and output of the algorithm, along
- with some other properties.
- """
-
- self.ts = datetime.now()
- self.db_variables = QgsSettings()
- self.interval_variables = ["Par année", "Par mois"]
- self.months_names_variables = [
- "Janvier",
- "Février",
- "Mars",
- "Avril",
- "Mai",
- "Juin",
- "Juillet",
- "Août",
- "Septembre",
- "Octobre",
- "Novembre",
- "Décembre",
- ]
- self.taxonomic_ranks_variables = ["Espèces", "Groupes taxonomiques"]
- self.agg_variables = ["Nombre de données", "Nombre d'espèces"]
-
- # Data base connection
- # db_param = QgsProcessingParameterString(
- # self.DATABASE,
- # self.tr("""CONNEXION À LA BASE DE DONNÉES
- # *1/ Sélectionnez votre connexion à la base de données LPO"""),
- # defaultValue='geonature_lpo'
- # )
- # db_param.setMetadata(
- # {
- # 'widget_wrapper': {'class': 'processing.gui.wrappers_postgis.ConnectionWidgetWrapper'}
- # }
- # )
- # self.addParameter(db_param)
- self.addParameter(
- QgsProcessingParameterProviderConnection(
- self.DATABASE,
- self.tr(
- """CONNEXION À LA BASE DE DONNÉES
- *1/ Sélectionnez votre connexion à la base de données LPO"""
- ),
- "postgres",
- defaultValue="geonature_lpo",
- )
- )
-
- # Input vector layer = study area
- self.addParameter(
- QgsProcessingParameterFeatureSource(
- self.STUDY_AREA,
- self.tr(
- """ZONE D'ÉTUDE
- *2/ Sélectionnez votre zone d'étude, à partir de laquelle seront extraits les résultats"""
- ),
- [QgsProcessing.TypeVectorPolygon],
- )
- )
-
- ### Time interval and period ###
- time_interval = QgsProcessingParameterEnum(
- self.TIME_INTERVAL,
- self.tr(
- """AGRÉGATION TEMPORELLE ET PÉRIODE
- *3/ Sélectionnez l'agrégation temporelle qui vous intéresse"""
- ),
- self.interval_variables,
- allowMultiple=False,
- )
- time_interval.setMetadata(
- {
- "widget_wrapper": {
- "useCheckBoxes": True,
- "columns": len(self.interval_variables),
- }
- }
- )
- self.addParameter(time_interval)
-
- add_five_years = QgsProcessingParameterEnum(
- self.ADD_FIVE_YEARS,
- self.tr(
- """4/ Si (et seulement si !) vous avez sélectionné l'agrégation Par année :
cochez la case ci-dessous si vous souhaitez ajouter des colonnes dîtes "bilan" par intervalle de 5 ans.
- N.B. : En cochant cette case, vous devez vous assurer de renseigner une période en années (cf. *5/) qui soit divisible par 5.
Exemple : 2011 - 2020."""
- ),
- [
- 'Oui, je souhaite ajouter des colonnes dîtes "bilan" par intervalle de 5 ans'
- ],
- allowMultiple=True,
- optional=True,
- )
- add_five_years.setMetadata(
- {"widget_wrapper": {"useCheckBoxes": True, "columns": 1}}
- )
- self.addParameter(add_five_years)
-
- self.addParameter(
- QgsProcessingParameterEnum(
- self.START_MONTH,
- self.tr(
- """*5/ Sélectionnez la période qui vous intéresse
- - Mois de début (nécessaire seulement si vous avez sélectionné l'agrégation Par mois) :"""
- ),
- self.months_names_variables,
- allowMultiple=False,
- optional=True,
- )
- )
-
- self.addParameter(
- QgsProcessingParameterNumber(
- self.START_YEAR,
- self.tr("- *Année de début :"),
- QgsProcessingParameterNumber.Integer,
- defaultValue=2010,
- minValue=1800,
- maxValue=int(self.ts.strftime("%Y")),
- )
- )
-
- self.addParameter(
- QgsProcessingParameterEnum(
- self.END_MONTH,
- self.tr(
- """- Mois de fin (nécessaire seulement si vous avez sélectionné l'agrégation Par mois) :"""
- ),
- self.months_names_variables,
- allowMultiple=False,
- optional=True,
- )
- )
-
- self.addParameter(
- QgsProcessingParameterNumber(
- self.END_YEAR,
- self.tr("- *Année de fin :"),
- QgsProcessingParameterNumber.Integer,
- defaultValue=self.ts.strftime("%Y"),
- minValue=1800,
- maxValue=int(self.ts.strftime("%Y")),
- )
- )
-
- # Taxonomic rank
- taxonomic_rank = QgsProcessingParameterEnum(
- self.TAXONOMIC_RANK,
- self.tr(
- """RANG TAXONOMIQUE
- *6/ Sélectionnez le rang taxonomique qui vous intéresse"""
- ),
- self.taxonomic_ranks_variables,
- allowMultiple=False,
- )
- taxonomic_rank.setMetadata(
- {
- "widget_wrapper": {
- "useCheckBoxes": True,
- "columns": len(self.taxonomic_ranks_variables),
- }
- }
- )
- self.addParameter(taxonomic_rank)
-
- # Aggregation type
- aggregation_type = QgsProcessingParameterEnum(
- self.AGG,
- self.tr(
- """AGRÉGATION DES RÉSULTATS
- *7/ Sélectionnez le type d'agrégation qui vous intéresse pour les résultats
- N.B. : Si vous avez choisi Espèces pour le rang taxonomique, Nombre de données sera utilisé par défaut"""
- ),
- self.agg_variables,
- allowMultiple=False,
- defaultValue="Nombre de données",
- )
- aggregation_type.setMetadata(
- {
- "widget_wrapper": {
- "useCheckBoxes": True,
- "columns": len(self.agg_variables),
- }
- }
- )
- self.addParameter(aggregation_type)
-
- ### Taxons filters ###
- self.addParameter(
- QgsProcessingParameterEnum(
- self.GROUPE_TAXO,
- self.tr(
- """FILTRES DE REQUÊTAGE
- 8/ Si cela vous intéresse, vous pouvez sélectionner un/plusieurs taxon(s) dans la liste déroulante suivante (à choix multiples)
pour filtrer vos données d'observations. Sinon, vous pouvez ignorer cette étape.
- N.B. : D'autres filtres taxonomiques sont disponibles dans les paramètres avancés (plus bas, juste avant l'enregistrement des résultats).
- - Groupes taxonomiques :"""
- ),
- self.db_variables.value("groupe_taxo"),
- allowMultiple=True,
- optional=True,
- )
- )
-
- regne = QgsProcessingParameterEnum(
- self.REGNE,
- self.tr("- Règnes :"),
- self.db_variables.value("regne"),
- allowMultiple=True,
- optional=True,
- )
- regne.setFlags(regne.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
- self.addParameter(regne)
-
- phylum = QgsProcessingParameterEnum(
- self.PHYLUM,
- self.tr("- Phylum :"),
- self.db_variables.value("phylum"),
- allowMultiple=True,
- optional=True,
- )
- phylum.setFlags(phylum.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
- self.addParameter(phylum)
-
- classe = QgsProcessingParameterEnum(
- self.CLASSE,
- self.tr("- Classe :"),
- self.db_variables.value("classe"),
- allowMultiple=True,
- optional=True,
- )
- classe.setFlags(classe.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
- self.addParameter(classe)
-
- ordre = QgsProcessingParameterEnum(
- self.ORDRE,
- self.tr("- Ordre :"),
- self.db_variables.value("ordre"),
- allowMultiple=True,
- optional=True,
- )
- ordre.setFlags(ordre.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
- self.addParameter(ordre)
-
- famille = QgsProcessingParameterEnum(
- self.FAMILLE,
- self.tr("- Famille :"),
- self.db_variables.value("famille"),
- allowMultiple=True,
- optional=True,
- )
- famille.setFlags(
- famille.flags() | QgsProcessingParameterDefinition.FlagAdvanced
- )
- self.addParameter(famille)
-
- group1_inpn = QgsProcessingParameterEnum(
- self.GROUP1_INPN,
- self.tr(
- "- Groupe 1 INPN (regroupement vernaculaire du référentiel national - niveau 1) :"
- ),
- self.db_variables.value("group1_inpn"),
- allowMultiple=True,
- optional=True,
- )
- group1_inpn.setFlags(
- group1_inpn.flags() | QgsProcessingParameterDefinition.FlagAdvanced
- )
- self.addParameter(group1_inpn)
-
- group2_inpn = QgsProcessingParameterEnum(
- self.GROUP2_INPN,
- self.tr(
- "- Groupe 2 INPN (regroupement vernaculaire du référentiel national - niveau 2) :"
- ),
- self.db_variables.value("group2_inpn"),
- allowMultiple=True,
- optional=True,
- )
- group2_inpn.setFlags(
- group2_inpn.flags() | QgsProcessingParameterDefinition.FlagAdvanced
- )
- self.addParameter(group2_inpn)
-
- # Extra "where" conditions
- extra_where = QgsProcessingParameterString(
- self.EXTRA_WHERE,
- self.tr(
- """Vous pouvez ajouter des conditions "where" supplémentaires dans l'encadré suivant, en langage SQL (commencez par and)"""
- ),
- multiLine=True,
- optional=True,
- )
- extra_where.setFlags(
- extra_where.flags() | QgsProcessingParameterDefinition.FlagAdvanced
- )
- self.addParameter(extra_where)
-
- # Output PostGIS layer = summary table
- self.addOutput(
- QgsProcessingOutputVectorLayer(
- self.OUTPUT,
- self.tr("Couche en sortie"),
- QgsProcessing.TypeVectorAnyGeometry,
- )
- )
-
- # Output PostGIS layer name
- self.addParameter(
- QgsProcessingParameterString(
- self.OUTPUT_NAME,
- self.tr(
- """PARAMÉTRAGE DES RESULTATS EN SORTIE
- *9/ Définissez un nom pour votre couche PostGIS"""
- ),
- self.tr("Tableau synthèse temps"),
- )
- )
-
- # Boolean : True = add the summary table in the DB ; False = don't
- self.addParameter(
- QgsProcessingParameterBoolean(
- self.ADD_TABLE,
- self.tr(
- "Enregistrer les résultats en sortie dans une nouvelle table PostgreSQL"
- ),
- False,
- )
- )
-
- ### Histogram ###
- add_histogram = QgsProcessingParameterBoolean(
- self.ADD_HISTOGRAM,
- self.tr(
- """Exporter les résultats sous la forme d'un histogramme du total par pas de temps choisi"""
- ),
- # [
- # "Oui, je souhaite exporter les résultats sous la forme d'un histogramme du total par pas de temps choisi"
- # ],
- # allowMultiple=True,
- optional=True,
- )
- # add_histogram.setMetadata(
- # {"widget_wrapper": {"useCheckBoxes": True, "columns": 1}}
- # )
- self.addParameter(add_histogram)
-
- self.addParameter(
- QgsProcessingParameterFileDestination(
- self.OUTPUT_HISTOGRAM,
- self.tr(
- """ENREGISTREMENT DES RESULTATS
- 11/ Si (et seulement si !) vous avez sélectionné l'export sous forme d'histogramme, veuillez renseigner un emplacement
pour l'enregistrer sur votre ordinateur (au format image). Dans le cas contraire, vous pouvez ignorer cette étape.
- Aide : Cliquez sur le bouton [...] puis sur 'Enregistrer vers un fichier...'"""
- ),
- self.tr("image PNG (*.png)"),
- optional=True,
- createByDefault=False,
- )
- )
-
- def processAlgorithm(self, parameters, context, feedback): # noqa N802
- """
- Here is where the processing itself takes place.
- """
-
- ### RETRIEVE PARAMETERS ###
- # Retrieve the input vector layer = study area
- study_area = self.parameterAsSource(parameters, self.STUDY_AREA, context)
- # Retrieve the output PostGIS layer name and format it
- layer_name = self.parameterAsString(parameters, self.OUTPUT_NAME, context)
- format_name = f"{layer_name} {str(self.ts.strftime('%Y%m%d_%H%M%S'))}"
- # Retrieve the time interval
- time_interval = self.interval_variables[
- self.parameterAsEnum(parameters, self.TIME_INTERVAL, context)
- ]
- # Retrieve the period
- start_year = self.parameterAsInt(parameters, self.START_YEAR, context)
- end_year = self.parameterAsInt(parameters, self.END_YEAR, context)
- if end_year < start_year:
- raise QgsProcessingException(
- "Veuillez renseigner une année de fin postérieure à l'année de début !"
- )
- # Retrieve the taxonomic rank
- taxonomic_rank = self.taxonomic_ranks_variables[
- self.parameterAsEnum(parameters, self.TAXONOMIC_RANK, context)
- ]
- # Retrieve the aggregation type
- aggregation_type = "Nombre de données"
- if taxonomic_rank == "Groupes taxonomiques":
- aggregation_type = self.agg_variables[
- self.parameterAsEnum(parameters, self.AGG, context)
- ]
- # Retrieve the taxons filters
- groupe_taxo = [
- self.db_variables.value("groupe_taxo")[i]
- for i in (self.parameterAsEnums(parameters, self.GROUPE_TAXO, context))
- ]
- regne = [
- self.db_variables.value("regne")[i]
- for i in (self.parameterAsEnums(parameters, self.REGNE, context))
- ]
- phylum = [
- self.db_variables.value("phylum")[i]
- for i in (self.parameterAsEnums(parameters, self.PHYLUM, context))
- ]
- classe = [
- self.db_variables.value("classe")[i]
- for i in (self.parameterAsEnums(parameters, self.CLASSE, context))
- ]
- ordre = [
- self.db_variables.value("ordre")[i]
- for i in (self.parameterAsEnums(parameters, self.ORDRE, context))
- ]
- famille = [
- self.db_variables.value("famille")[i]
- for i in (self.parameterAsEnums(parameters, self.FAMILLE, context))
- ]
- group1_inpn = [
- self.db_variables.value("group1_inpn")[i]
- for i in (self.parameterAsEnums(parameters, self.GROUP1_INPN, context))
- ]
- group2_inpn = [
- self.db_variables.value("group2_inpn")[i]
- for i in (self.parameterAsEnums(parameters, self.GROUP2_INPN, context))
- ]
- # Retrieve the extra "where" conditions
- extra_where = self.parameterAsString(parameters, self.EXTRA_WHERE, context)
- # Retrieve the histogram parameter
- add_histogram = self.parameterAsEnums(parameters, self.ADD_HISTOGRAM, context)
- if len(add_histogram) > 0:
- output_histogram = self.parameterAsFileOutput(
- parameters, self.OUTPUT_HISTOGRAM, context
- )
- if output_histogram == "":
- raise QgsProcessingException(
- "Veuillez renseigner un emplacement pour enregistrer votre histogramme !"
- )
-
- ### CONSTRUCT "SELECT" CLAUSE (SQL) ###
- # Select data according to the time interval and the period
- select_data, x_var = construct_sql_select_data_per_time_interval(
- self,
- time_interval,
- start_year,
- end_year,
- aggregation_type,
- parameters,
- context,
- )
- # Select species info (optional)
- select_species_info = """/*source_id_sp, */obs.cd_nom, obs.cd_ref, nom_rang as "Rang", groupe_taxo AS "Groupe taxo",
- obs.nom_vern AS "Nom vernaculaire", nom_sci AS "Nom scientifique\""""
- # Select taxonomic groups info (optional)
- select_taxo_groups_info = 'groupe_taxo AS "Groupe taxo"'
- ### CONSTRUCT "WHERE" CLAUSE (SQL) ###
- # Construct the sql array containing the study area's features geometry
- array_polygons = construct_sql_array_polygons(study_area)
- # Define the "where" clause of the SQL query, aiming to retrieve the output PostGIS layer = summary table
- where = f"is_valid and is_present and ST_intersects(obs.geom, ST_union({array_polygons}))"
- # Define a dictionnary with the aggregated taxons filters and complete the "where" clause thanks to it
- taxons_filters = {
- "groupe_taxo": groupe_taxo,
- "regne": regne,
- "phylum": phylum,
- "classe": classe,
- "ordre": ordre,
- "famille": famille,
- "obs.group1_inpn": group1_inpn,
- "obs.group2_inpn": group2_inpn,
- }
- # taxons_where = construct_sql_taxons_filter(taxons_filters)
- # where += taxons_where
- # # Complete the "where" clause with the extra conditions
- # where += " " + extra_where
- ### CONSTRUCT "GROUP BY" CLAUSE (SQL) ###
- # Group by species (optional)
- group_by_species = (
- "/*source_id_sp, */obs.cd_nom, obs.cd_ref, nom_rang, nom_sci, obs.nom_vern, "
- if taxonomic_rank == "Espèces"
- else ""
- )
-
- ### EXECUTE THE SQL QUERY ###
- # Retrieve the data base connection name
- connection = self.parameterAsString(parameters, self.DATABASE, context)
- # URI --> Configures connection to database and the SQL query
- # uri = postgis.uri_from_name(connection)
- uri = uri_from_name(connection)
- # Define the SQL query
- query = f"""SELECT row_number() OVER () AS id, {select_species_info if taxonomic_rank == 'Espèces' else select_taxo_groups_info}{select_data}
- FROM src_lpodatas.v_c_observations_light obs
- LEFT JOIN taxonomie.bib_taxref_rangs r ON obs.id_rang = r.id_rang
- WHERE {where}
- GROUP BY {group_by_species}groupe_taxo
- ORDER BY groupe_taxo{ ", obs.nom_vern" if taxonomic_rank == 'Espèces' else ''}"""
-
- feedback.pushDebugInfo(query)
- # feedback.pushInfo(query)
- # Retrieve the boolean add_table
- add_table = self.parameterAsBool(parameters, self.ADD_TABLE, context)
- if add_table:
- # Define the name of the PostGIS summary table which will be created in the DB
- table_name = simplify_name(format_name)
- # Define the SQL queries
- queries = construct_queries_list(table_name, query)
- # Execute the SQL queries
- execute_sql_queries(context, feedback, connection, queries)
- # Format the URI
- uri.setDataSource(None, table_name, None, "", "id")
- else:
- # Format the URI with the query
- uri.setDataSource("", "(" + query + ")", None, "", "id")
-
- ### GET THE OUTPUT LAYER ###
- # Retrieve the output PostGIS layer = summary table
- self.layer_summary = QgsVectorLayer(uri.uri(), format_name, "postgres")
- # Check if the PostGIS layer is valid
- check_layer_is_valid(feedback, self.layer_summary)
- # Load the PostGIS layer
- load_layer(context, self.layer_summary)
- # Add action to layer
- # with open(os.path.join(plugin_path, "format_csv.py"), "r") as file:
- # action_code = file.read()
- # action = QgsAction(
- # QgsAction.GenericPython,
- # "Exporter la couche sous format Excel dans mon dossier utilisateur avec la mise en forme adaptée",
- # action_code,
- # os.path.join(plugin_path, "icons", "excel.png"),
- # False,
- # "Exporter sous format Excel",
- # {"Layer"},
- # )
- # self.layer_summary.actions().addAction(action)
- # # JOKE
- # with open(os.path.join(plugin_path, "joke.py"), "r") as file:
- # joke_action_code = file.read()
- # joke_action = QgsAction(
- # QgsAction.GenericPython,
- # "Rédiger mon rapport",
- # joke_action_code,
- # os.path.join(plugin_path, "icons", "logo_LPO.png"),
- # False,
- # "Rédiger mon rapport",
- # {"Layer"},
- # )
- # self.layer_summary.actions().addAction(joke_action)
-
- ### CONSTRUCT THE HISTOGRAM ###
- if len(add_histogram) > 0:
- plt.close()
- y_var = []
- for x in x_var:
- y = 0
- for feature in self.layer_summary.getFeatures():
- y += feature[x]
- y_var.append(y)
- if len(x_var) <= 20:
- plt.subplots_adjust(bottom=0.4)
- elif len(x_var) <= 80:
- plt.figure(figsize=(20, 8))
- plt.subplots_adjust(bottom=0.3, left=0.05, right=0.95)
- else:
- plt.figure(figsize=(40, 16))
- plt.subplots_adjust(bottom=0.2, left=0.03, right=0.97)
- plt.bar(range(len(x_var)), y_var, tick_label=x_var)
- plt.xticks(rotation="vertical")
- x_label = time_interval.split(" ")[1].title()
- if x_label[-1] != "s":
- x_label += "s"
- plt.xlabel(x_label)
- plt.ylabel(aggregation_type)
- plt.title(
- f"{aggregation_type} {(time_interval[0].lower() + time_interval[1:])}"
- )
- if output_histogram[-4:] != ".png":
- output_histogram += ".png"
- plt.savefig(output_histogram)
- # plt.show()
-
- return {self.OUTPUT: self.layer_summary.id()}
-
- def postProcessAlgorithm(self, _context, _feedback) -> Dict: # noqa N802
- # Open the attribute table of the PostGIS layer
- iface.showAttributeTable(self.layer_summary)
- iface.setActiveLayer(self.layer_summary)
-
- return {}
-
- def tr(self, string: str) -> str:
- return QCoreApplication.translate("Processing", string)
-
- def createInstance(self): # noqa N802
- return type(self)()
diff --git a/plugin_qgis_lpo/resources/help/index.html b/plugin_qgis_lpo/resources/help/index.html
new file mode 100644
index 0000000..77c455d
--- /dev/null
+++ b/plugin_qgis_lpo/resources/help/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+ Redirecting...
+
+
+
+
+
+ Redirection to the online documentation...
+
+
+
+
diff --git a/plugin_qgis_lpo/resources/i18n/plugin_translation.pro b/plugin_qgis_lpo/resources/i18n/plugin_translation.pro
new file mode 100644
index 0000000..a6d63b9
--- /dev/null
+++ b/plugin_qgis_lpo/resources/i18n/plugin_translation.pro
@@ -0,0 +1,8 @@
+FORMS = ../../gui/dlg_settings.ui
+
+SOURCES= ../../plugin_main.py \
+ ../../gui/dlg_settings.py \
+ ../../toolbelt/log_handler.py \
+ ../../toolbelt/preferences.py
+
+TRANSLATIONS = plugin_qgis_lpo_en.ts
diff --git a/plugin_qgis_lpo/resources/images/default_icon.png b/plugin_qgis_lpo/resources/images/default_icon.png
new file mode 100644
index 0000000..89a7044
Binary files /dev/null and b/plugin_qgis_lpo/resources/images/default_icon.png differ
diff --git a/plugin_qgis_lpo/icons/excel.png b/plugin_qgis_lpo/resources/images/excel.png
similarity index 100%
rename from plugin_qgis_lpo/icons/excel.png
rename to plugin_qgis_lpo/resources/images/excel.png
diff --git a/plugin_qgis_lpo/icons/extract_data.png b/plugin_qgis_lpo/resources/images/extract_data.png
similarity index 100%
rename from plugin_qgis_lpo/icons/extract_data.png
rename to plugin_qgis_lpo/resources/images/extract_data.png
diff --git a/plugin_qgis_lpo/icons/histogram.png b/plugin_qgis_lpo/resources/images/histogram.png
similarity index 100%
rename from plugin_qgis_lpo/icons/histogram.png
rename to plugin_qgis_lpo/resources/images/histogram.png
diff --git a/plugin_qgis_lpo/icons/logo_LPO.png b/plugin_qgis_lpo/resources/images/logo_LPO.png
similarity index 100%
rename from plugin_qgis_lpo/icons/logo_LPO.png
rename to plugin_qgis_lpo/resources/images/logo_LPO.png
diff --git a/plugin_qgis_lpo/icons/logo_lpo_aura.png b/plugin_qgis_lpo/resources/images/logo_lpo_aura.png
similarity index 100%
rename from plugin_qgis_lpo/icons/logo_lpo_aura.png
rename to plugin_qgis_lpo/resources/images/logo_lpo_aura.png
diff --git a/plugin_qgis_lpo/icons/logo_lpo_aura_carre.png b/plugin_qgis_lpo/resources/images/logo_lpo_aura_carre.png
similarity index 100%
rename from plugin_qgis_lpo/icons/logo_lpo_aura_carre.png
rename to plugin_qgis_lpo/resources/images/logo_lpo_aura_carre.png
diff --git a/plugin_qgis_lpo/icons/map.png b/plugin_qgis_lpo/resources/images/map.png
similarity index 100%
rename from plugin_qgis_lpo/icons/map.png
rename to plugin_qgis_lpo/resources/images/map.png
diff --git a/plugin_qgis_lpo/icons/table.png b/plugin_qgis_lpo/resources/images/table.png
similarity index 100%
rename from plugin_qgis_lpo/icons/table.png
rename to plugin_qgis_lpo/resources/images/table.png
diff --git a/plugin_qgis_lpo/icons/word.png b/plugin_qgis_lpo/resources/images/word.png
similarity index 100%
rename from plugin_qgis_lpo/icons/word.png
rename to plugin_qgis_lpo/resources/images/word.png
diff --git a/plugin_qgis_lpo/styles/reproduction.qml b/plugin_qgis_lpo/resources/styles/reproduction.qml
similarity index 100%
rename from plugin_qgis_lpo/styles/reproduction.qml
rename to plugin_qgis_lpo/resources/styles/reproduction.qml
diff --git a/plugin_qgis_lpo/toolbelt/__init__.py b/plugin_qgis_lpo/toolbelt/__init__.py
new file mode 100644
index 0000000..435f023
--- /dev/null
+++ b/plugin_qgis_lpo/toolbelt/__init__.py
@@ -0,0 +1,3 @@
+#! python3 # noqa: E265
+from .log_handler import PlgLogger # noqa: F401
+from .preferences import PlgOptionsManager # noqa: F401
diff --git a/plugin_qgis_lpo/toolbelt/log_handler.py b/plugin_qgis_lpo/toolbelt/log_handler.py
new file mode 100644
index 0000000..96de803
--- /dev/null
+++ b/plugin_qgis_lpo/toolbelt/log_handler.py
@@ -0,0 +1,154 @@
+#! python3 # noqa: E265
+
+# standard library
+import logging
+from functools import partial
+from typing import Callable
+
+# PyQGIS
+from qgis.core import QgsMessageLog, QgsMessageOutput
+from qgis.gui import QgsMessageBar
+from qgis.PyQt.QtWidgets import QPushButton, QWidget
+from qgis.utils import iface
+
+import plugin_qgis_lpo.toolbelt.preferences as plg_prefs_hdlr
+
+# project package
+from plugin_qgis_lpo.__about__ import __title__
+
+# ############################################################################
+# ########## Classes ###############
+# ##################################
+
+
+class PlgLogger(logging.Handler):
+ """Python logging handler supercharged with QGIS useful methods."""
+
+ @staticmethod
+ def log(
+ message: str,
+ application: str = __title__,
+ log_level: int = 0,
+ push: bool = False,
+ duration: int = None,
+ # widget
+ button: bool = False,
+ button_text: str = None,
+ button_connect: Callable = None,
+ # parent
+ parent_location: QWidget = None,
+ ):
+ """Send messages to QGIS messages windows and to the user as a message bar. \
+ Plugin name is used as title. If debug mode is disabled, only warnings (1) and \
+ errors (2) or with push are sent.
+
+ :param message: message to display
+ :type message: str
+ :param application: name of the application sending the message. \
+ Defaults to __about__.__title__
+ :type application: str, optional
+ :param log_level: message level. Possible values: 0 (info), 1 (warning), \
+ 2 (critical), 3 (success), 4 (none - grey). Defaults to 0 (info)
+ :type log_level: int, optional
+ :param push: also display the message in the QGIS message bar in addition to \
+ the log, defaults to False
+ :type push: bool, optional
+ :param duration: duration of the message in seconds. If not set, the \
+ duration is calculated from the log level: `(log_level + 1) * 3`. seconds. \
+ If set to 0, then the message must be manually dismissed by the user. \
+ Defaults to None.
+ :type duration: int, optional
+ :param button: display a button in the message bar. Defaults to False.
+ :type button: bool, optional
+ :param button_text: text label of the button. Defaults to None.
+ :type button_text: str, optional
+ :param button_connect: function to be called when the button is pressed. \
+ If not set, a simple dialog (QgsMessageOutput) is used to dislay the message. \
+ Defaults to None.
+ :type button_connect: Callable, optional
+ :param parent_location: parent location widget. \
+ If not set, QGIS canvas message bar is used to push message, \
+ otherwise if a QgsMessageBar is available in parent_location it is used instead. \
+ Defaults to None.
+ :type parent_location: Widget, optional
+
+ :Example:
+
+ .. code-block:: python
+
+ log(message="Plugin loaded - INFO", log_level=0, push=False)
+ log(message="Plugin loaded - WARNING", log_level=1, push=1, duration=5)
+ log(message="Plugin loaded - ERROR", log_level=2, push=1, duration=0)
+ log(
+ message="Plugin loaded - SUCCESS",
+ log_level=3,
+ push=1,
+ duration=10,
+ button=True
+ )
+ log(message="Plugin loaded - TEST", log_level=4, push=0)
+ """
+ # if not debug mode and not push, let's ignore INFO, SUCCESS and TEST
+ debug_mode = plg_prefs_hdlr.PlgOptionsManager.get_plg_settings().debug_mode
+ if not debug_mode and not push and (log_level < 1 or log_level > 2):
+ return
+
+ # ensure message is a string
+ if not isinstance(message, str):
+ try:
+ message = str(message)
+ except Exception as err:
+ err_msg = "Log message must be a string, not: {}. Trace: {}".format(
+ type(message), err
+ )
+ logging.error(err_msg)
+ message = err_msg
+
+ # send it to QGIS messages panel
+ QgsMessageLog.logMessage(
+ message=message, tag=application, notifyUser=push, level=log_level
+ )
+
+ # optionally, display message on QGIS Message bar (above the map canvas)
+ if push and iface is not None:
+ msg_bar = None
+
+ # QGIS or custom dialog
+ if parent_location and isinstance(parent_location, QWidget):
+ msg_bar = parent_location.findChild(QgsMessageBar)
+
+ if not msg_bar:
+ msg_bar = iface.messageBar()
+
+ # calc duration
+ if duration is None:
+ duration = (log_level + 1) * 3
+
+ # create message with/out a widget
+ if button:
+ # create output message
+ notification = iface.messageBar().createMessage(
+ title=application, text=message
+ )
+ widget_button = QPushButton(button_text or "More...")
+ if button_connect:
+ widget_button.clicked.connect(button_connect)
+ else:
+ mini_dlg = QgsMessageOutput.createMessageOutput()
+ mini_dlg.setTitle(application)
+ mini_dlg.setMessage(message, QgsMessageOutput.MessageText)
+ widget_button.clicked.connect(partial(mini_dlg.showMessage, False))
+
+ notification.layout().addWidget(widget_button)
+ msg_bar.pushWidget(
+ widget=notification, level=log_level, duration=duration
+ )
+
+ else:
+ # send simple message
+ msg_bar.pushMessage(
+ title=application,
+ text=message,
+ level=log_level,
+ duration=duration,
+ )
diff --git a/plugin_qgis_lpo/toolbelt/preferences.py b/plugin_qgis_lpo/toolbelt/preferences.py
new file mode 100644
index 0000000..9ad585f
--- /dev/null
+++ b/plugin_qgis_lpo/toolbelt/preferences.py
@@ -0,0 +1,145 @@
+#! python3 # noqa: E265
+
+"""
+ Plugin settings.
+"""
+
+# standard
+from dataclasses import asdict, dataclass, fields
+
+# PyQGIS
+from qgis.core import QgsSettings
+
+# package
+import plugin_qgis_lpo.toolbelt.log_handler as log_hdlr
+from plugin_qgis_lpo.__about__ import __title__, __version__
+
+# ############################################################################
+# ########## Classes ###############
+# ##################################
+
+
+@dataclass
+class PlgSettingsStructure:
+ """Plugin settings structure and defaults values."""
+
+ # global
+ debug_mode: bool = False
+ version: str = __version__
+
+
+class PlgOptionsManager:
+ @staticmethod
+ def get_plg_settings() -> PlgSettingsStructure:
+ """Load and return plugin settings as a dictionary. \
+ Useful to get user preferences across plugin logic.
+
+ :return: plugin settings
+ :rtype: PlgSettingsStructure
+ """
+ # get dataclass fields definition
+ settings_fields = fields(PlgSettingsStructure)
+
+ # retrieve settings from QGIS/Qt
+ settings = QgsSettings()
+ settings.beginGroup(__title__)
+
+ # map settings values to preferences object
+ li_settings_values = []
+ for i in settings_fields:
+ li_settings_values.append(
+ settings.value(key=i.name, defaultValue=i.default, type=i.type)
+ )
+
+ # instanciate new settings object
+ options = PlgSettingsStructure(*li_settings_values)
+
+ settings.endGroup()
+
+ return options
+
+ @staticmethod
+ def get_value_from_key(key: str, default=None, exp_type=None):
+ """Load and return plugin settings as a dictionary. \
+ Useful to get user preferences across plugin logic.
+
+ :return: plugin settings value matching key
+ """
+ if not hasattr(PlgSettingsStructure, key):
+ log_hdlr.PlgLogger.log(
+ message="Bad settings key. Must be one of: {}".format(
+ ",".join(PlgSettingsStructure._fields)
+ ),
+ log_level=1,
+ )
+ return None
+
+ settings = QgsSettings()
+ settings.beginGroup(__title__)
+
+ try:
+ out_value = settings.value(key=key, defaultValue=default, type=exp_type)
+ except Exception as err:
+ log_hdlr.PlgLogger.log(
+ message="Error occurred trying to get settings: {}.Trace: {}".format(
+ key, err
+ )
+ )
+ out_value = None
+
+ settings.endGroup()
+
+ return out_value
+
+ @classmethod
+ def set_value_from_key(cls, key: str, value) -> bool:
+ """Set plugin QSettings value using the key.
+
+ :param key: QSettings key
+ :type key: str
+ :param value: value to set
+ :type value: depending on the settings
+ :return: operation status
+ :rtype: bool
+ """
+ if not hasattr(PlgSettingsStructure, key):
+ log_hdlr.PlgLogger.log(
+ message="Bad settings key. Must be one of: {}".format(
+ ",".join(PlgSettingsStructure._fields)
+ ),
+ log_level=2,
+ )
+ return False
+
+ settings = QgsSettings()
+ settings.beginGroup(__title__)
+
+ try:
+ settings.setValue(key, value)
+ out_value = True
+ except Exception as err:
+ log_hdlr.PlgLogger.log(
+ message="Error occurred trying to set settings: {}.Trace: {}".format(
+ key, err
+ )
+ )
+ out_value = False
+
+ settings.endGroup()
+
+ return out_value
+
+ @classmethod
+ def save_from_object(cls, plugin_settings_obj: PlgSettingsStructure):
+ """Load and return plugin settings as a dictionary. \
+ Useful to get user preferences across plugin logic.
+
+ :return: plugin settings value matching key
+ """
+ settings = QgsSettings()
+ settings.beginGroup(__title__)
+
+ for k, v in asdict(plugin_settings_obj).items():
+ cls.set_value_from_key(k, v)
+
+ settings.endGroup()
diff --git a/pyproject.toml b/pyproject.toml
deleted file mode 100644
index 7070a80..0000000
--- a/pyproject.toml
+++ /dev/null
@@ -1,14 +0,0 @@
-[tool.black]
-line-length = 88
-target-version = ['py38']
-include = '\.pyi?$'
-
-[tool.isort]
-# Black compatible values for isort https://black.readthedocs.io/en/stable/compatible_configs.html#isort
-profile = "black"
-
-[tool.pytest.ini_options]
-addopts = "-v"
-
-[tool.coverage.report]
-omit = ["plugin_qgis_lpo/qgis_plugin_tools/*"]
diff --git a/requirements-dev.in b/requirements-dev.in
deleted file mode 100644
index 146b488..0000000
--- a/requirements-dev.in
+++ /dev/null
@@ -1,24 +0,0 @@
-# Debugging
-debugpy
-
-# Dependency maintenance
-pip-tools
-
-# Testing
-pytest
-pytest-cov
-pytest-qgis
-
-# Linting and formatting
-pre-commit
-black
-isort
-mypy
-flake8
-flake8-bugbear
-pep8-naming
-flake8-annotations
-flake8-qgis
-
-# Stubs
-PyQt5-stubs
diff --git a/requirements-dev.txt b/requirements-dev.txt
deleted file mode 100644
index 021fde4..0000000
--- a/requirements-dev.txt
+++ /dev/null
@@ -1,109 +0,0 @@
-#
-# This file is autogenerated by pip-compile with Python 3.11
-# by the following command:
-#
-# pip-compile --output-file=requirements-dev.txt requirements-dev.in
-#
-astor==0.8.1
- # via flake8-qgis
-attrs==23.2.0
- # via
- # flake8-annotations
- # flake8-bugbear
-black==23.12.1
- # via -r requirements-dev.in
-build==1.0.3
- # via pip-tools
-cfgv==3.4.0
- # via pre-commit
-click==8.1.7
- # via
- # black
- # pip-tools
-coverage[toml]==7.4.0
- # via
- # coverage
- # pytest-cov
-debugpy==1.8.0
- # via -r requirements-dev.in
-distlib==0.3.8
- # via virtualenv
-filelock==3.13.1
- # via virtualenv
-flake8==7.0.0
- # via
- # -r requirements-dev.in
- # flake8-annotations
- # flake8-bugbear
- # flake8-qgis
- # pep8-naming
-flake8-annotations==3.0.1
- # via -r requirements-dev.in
-flake8-bugbear==24.1.17
- # via -r requirements-dev.in
-flake8-qgis==1.0.0
- # via -r requirements-dev.in
-identify==2.5.33
- # via pre-commit
-iniconfig==2.0.0
- # via pytest
-isort==5.13.2
- # via -r requirements-dev.in
-mccabe==0.7.0
- # via flake8
-mypy==1.8.0
- # via -r requirements-dev.in
-mypy-extensions==1.0.0
- # via
- # black
- # mypy
-nodeenv==1.8.0
- # via pre-commit
-packaging==23.2
- # via
- # black
- # build
- # pytest
-pathspec==0.12.1
- # via black
-pep8-naming==0.13.3
- # via -r requirements-dev.in
-pip-tools==7.3.0
- # via -r requirements-dev.in
-platformdirs==4.1.0
- # via
- # black
- # virtualenv
-pluggy==1.3.0
- # via pytest
-pre-commit==3.6.0
- # via -r requirements-dev.in
-pycodestyle==2.11.1
- # via flake8
-pyflakes==3.2.0
- # via flake8
-pyproject-hooks==1.0.0
- # via build
-pyqt5-stubs==5.15.6.0
- # via -r requirements-dev.in
-pytest==7.4.4
- # via
- # -r requirements-dev.in
- # pytest-cov
- # pytest-qgis
-pytest-cov==4.1.0
- # via -r requirements-dev.in
-pytest-qgis==2.0.0
- # via -r requirements-dev.in
-pyyaml==6.0.1
- # via pre-commit
-typing-extensions==4.9.0
- # via mypy
-virtualenv==20.25.0
- # via pre-commit
-wheel==0.42.0
- # via pip-tools
-
-# The following packages are considered to be unsafe in a requirements file:
-# pip
-# setuptools
diff --git a/requirements/development.txt b/requirements/development.txt
new file mode 100644
index 0000000..599bada
--- /dev/null
+++ b/requirements/development.txt
@@ -0,0 +1,10 @@
+# Develoment dependencies
+# -----------------------
+
+black
+
+flake8-builtins>=1.5,<2.3
+flake8-isort>=4.1,<6.2
+flake8-qgis>=1,<1.1
+isort>=5.8,<5.14
+pre-commit>=3,<4
diff --git a/requirements/documentation.txt b/requirements/documentation.txt
new file mode 100644
index 0000000..8fc973d
--- /dev/null
+++ b/requirements/documentation.txt
@@ -0,0 +1,7 @@
+# Documentation (for devs)
+# -----------------------
+
+myst-parser[linkify]>=1,<3
+sphinx-autobuild==2021.*
+sphinx-copybutton>=0.2,<1
+sphinx-rtd-theme>=1,<3
diff --git a/requirements/packaging.txt b/requirements/packaging.txt
new file mode 100644
index 0000000..bd00b58
--- /dev/null
+++ b/requirements/packaging.txt
@@ -0,0 +1,4 @@
+# Packaging
+# ---------
+
+qgis-plugin-ci>=2.6,<3
diff --git a/requirements/testing.txt b/requirements/testing.txt
new file mode 100644
index 0000000..e8400b1
--- /dev/null
+++ b/requirements/testing.txt
@@ -0,0 +1,5 @@
+# Testing dependencies
+# --------------------
+
+pytest-cov>=3,<5
+packaging>=23
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..99a419b
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,82 @@
+# -- Packaging --------------------------------------
+[metadata]
+description-file = README.md
+
+[qgis-plugin-ci]
+plugin_path = plugin_qgis_lpo
+project_slug = plugin-qgis-lpo
+
+github_organization_slug = lpoaura
+
+
+# -- Code quality ------------------------------------
+
+[flake8]
+count = True
+exclude =
+ # No need to traverse our git directory
+ .git,
+ # There's no value in checking cache directories
+ __pycache__,
+ # The conf file is mostly autogenerated, ignore it
+ docs/conf.py,
+ # The old directory contains Flake8 2.0
+ old,
+ # This contains our built documentation
+ build,
+ # This contains builds of flake8 that we don't want to check
+ dist,
+ # This contains local virtual environments
+ .venv*,
+ # do not watch on tests
+ tests,
+ # do not consider external packages
+ */external/*, ext_libs/*
+ignore = E121,E123,E126,E203,E226,E24,E704,QGS105,W503,W504
+max-complexity = 15
+max-doc-length = 130
+max-line-length = 100
+output-file = dev_flake8_report.txt
+statistics = True
+tee = True
+builtins-ignorelist = ["id"]
+
+
+[isort]
+ensure_newline_before_comments = True
+force_grid_wrap = 0
+include_trailing_comma = True
+line_length = 88
+multi_line_output = 3
+profile = black
+use_parentheses = True
+
+# -- Tests ----------------------------------------------
+[tool:pytest]
+addopts =
+ --junitxml=junit/test-results.xml
+ --cov-config=setup.cfg
+ --cov=plugin_qgis_lpo
+ --cov-report=html
+ --cov-report=term
+ --cov-report=xml
+ --ignore=tests/_wip/
+norecursedirs = .* build dev development dist docs CVS fixtures _darcs {arch} *.egg venv _wip
+python_files = test_*.py
+testpaths = tests
+
+[coverage:run]
+branch = True
+omit =
+ .venv/*
+ *tests*
+
+[coverage:report]
+exclude_lines =
+ if self.debug:
+ pragma: no cover
+ raise NotImplementedError
+ if __name__ == .__main__.:
+
+ignore_errors = True
+show_missing = True
diff --git a/test/conftest.py b/test/conftest.py
deleted file mode 100644
index f420bbf..0000000
--- a/test/conftest.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""
-This class contains fixtures and common helper function to keep the test files
-shorter.
-
-pytest-qgis (https://pypi.org/project/pytest-qgis) contains the following helpful
-fixtures:
-
-* qgis_app initializes and returns fully configured QgsApplication.
- This fixture is called automatically on the start of pytest session.
-* qgis_canvas initializes and returns QgsMapCanvas
-* qgis_iface returns mocked QgsInterface
-* new_project makes sure that all the map layers and configurations are removed.
- This should be used with tests that add stuff to QgsProject.
-
-"""
diff --git a/test/pytest.ini b/test/pytest.ini
deleted file mode 100644
index eea2c18..0000000
--- a/test/pytest.ini
+++ /dev/null
@@ -1 +0,0 @@
-[pytest]
diff --git a/test/test_plugin.py b/test/test_plugin.py
deleted file mode 100644
index 6d8e568..0000000
--- a/test/test_plugin.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from plugin_qgis_lpo.qgis_plugin_tools.tools.resources import plugin_name
-
-
-def test_plugin_name():
- assert plugin_name() == "PluginQGISLPO"
diff --git a/.gitmodules b/tests/__init__.py
similarity index 100%
rename from .gitmodules
rename to tests/__init__.py
diff --git a/tests/qgis/__init__.py b/tests/qgis/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/qgis/test_helpers.py b/tests/qgis/test_helpers.py
new file mode 100644
index 0000000..668536c
--- /dev/null
+++ b/tests/qgis/test_helpers.py
@@ -0,0 +1,38 @@
+#! python3 # noqa E265
+
+"""
+ Usage from the repo root folder:
+
+ .. code-block:: bash
+
+ # for whole tests
+ python -m unittest tests.qgis.test_plg_preferences
+ # for specific test
+ python -m unittest tests.qgis.test_plg_preferences.TestPlgPreferences.test_plg_preferences_structure
+"""
+
+# standard library
+from qgis.testing import unittest
+
+# project
+from plugin_qgis_lpo.commons.helpers import simplify_name
+
+# ############################################################################
+# ########## Classes #############
+# ################################
+
+
+class TestHelpers(unittest.TestCase):
+ def test_simplify_name(self):
+ """Test settings types and default values."""
+ string = "Table des espèces d'oiseaux 20/03/2023"
+
+ # global
+ self.assertEqual(simplify_name(string), "table_des_especes_doiseaux_20032023")
+
+
+# ############################################################################
+# ####### Stand-alone run ########
+# ################################
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/qgis/test_plg_preferences.py b/tests/qgis/test_plg_preferences.py
new file mode 100644
index 0000000..9b91b90
--- /dev/null
+++ b/tests/qgis/test_plg_preferences.py
@@ -0,0 +1,45 @@
+#! python3 # noqa E265
+
+"""
+ Usage from the repo root folder:
+
+ .. code-block:: bash
+
+ # for whole tests
+ python -m unittest tests.qgis.test_plg_preferences
+ # for specific test
+ python -m unittest tests.qgis.test_plg_preferences.TestPlgPreferences.test_plg_preferences_structure
+"""
+
+# standard library
+from qgis.testing import unittest
+
+# project
+from plugin_qgis_lpo.__about__ import __version__
+from plugin_qgis_lpo.toolbelt.preferences import PlgSettingsStructure
+
+# ################################
+# ########## Classes #############
+# ################################
+
+
+class TestPlgPreferences(unittest.TestCase):
+ def test_plg_preferences_structure(self):
+ """Test settings types and default values."""
+ settings = PlgSettingsStructure()
+
+ # global
+ self.assertTrue(hasattr(settings, "debug_mode"))
+ self.assertIsInstance(settings.debug_mode, bool)
+ self.assertEqual(settings.debug_mode, False)
+
+ self.assertTrue(hasattr(settings, "version"))
+ self.assertIsInstance(settings.version, str)
+ self.assertEqual(settings.version, __version__)
+
+
+# ############################################################################
+# ####### Stand-alone run ########
+# ################################
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/test_plg_metadata.py b/tests/unit/test_plg_metadata.py
new file mode 100644
index 0000000..85e0e56
--- /dev/null
+++ b/tests/unit/test_plg_metadata.py
@@ -0,0 +1,83 @@
+#! python3 # noqa E265
+
+"""
+ Usage from the repo root folder:
+
+ .. code-block:: bash
+ # for whole tests
+ python -m unittest tests.unit.test_plg_metadata
+ # for specific test
+ python -m unittest tests.unit.test_plg_metadata.TestPluginMetadata.test_version_semver
+"""
+
+# standard library
+import unittest
+from pathlib import Path
+
+# 3rd party
+from packaging.version import parse
+
+# project
+from plugin_qgis_lpo import __about__
+
+# ############################################################################
+# ########## Classes #############
+# ################################
+
+
+class TestPluginMetadata(unittest.TestCase):
+
+ """Test about module"""
+
+ def test_metadata_types(self):
+ """Test types."""
+ # plugin metadata.txt file
+ self.assertIsInstance(__about__.PLG_METADATA_FILE, Path)
+ self.assertTrue(__about__.PLG_METADATA_FILE.is_file())
+
+ # plugin dir
+ self.assertIsInstance(__about__.DIR_PLUGIN_ROOT, Path)
+ self.assertTrue(__about__.DIR_PLUGIN_ROOT.is_dir())
+
+ # metadata as dict
+ self.assertIsInstance(__about__.__plugin_md__, dict)
+
+ # general
+ self.assertIsInstance(__about__.__author__, str)
+ self.assertIsInstance(__about__.__copyright__, str)
+ self.assertIsInstance(__about__.__email__, str)
+ self.assertIsInstance(__about__.__keywords__, list)
+ self.assertIsInstance(__about__.__license__, str)
+ self.assertIsInstance(__about__.__summary__, str)
+ self.assertIsInstance(__about__.__title__, str)
+ self.assertIsInstance(__about__.__title_clean__, str)
+ self.assertIsInstance(__about__.__version__, str)
+ self.assertIsInstance(__about__.__version_info__, tuple)
+
+ # misc
+ self.assertLessEqual(len(__about__.__title_clean__), len(__about__.__title__))
+
+ # QGIS versions
+ self.assertIsInstance(
+ __about__.__plugin_md__.get("general").get("qgisminimumversion"), str
+ )
+
+ self.assertIsInstance(
+ __about__.__plugin_md__.get("general").get("qgismaximumversion"), str
+ )
+
+ self.assertLessEqual(
+ float(__about__.__plugin_md__.get("general").get("qgisminimumversion")),
+ float(__about__.__plugin_md__.get("general").get("qgismaximumversion")),
+ )
+
+ def test_version_semver(self):
+ """Test if version comply with semantic versioning."""
+ self.assertTrue(parse(__about__.__version__))
+
+
+# ############################################################################
+# ####### Stand-alone run ########
+# ################################
+if __name__ == "__main__":
+ unittest.main()