diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 98effff4b..a8dbcbe67 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,14 +13,14 @@ jobs: uses: psf/black@stable with: src: "setup.py ./backend/gn_module_monitoring" + frontend: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 + - uses: actions/setup-node@v3 - name: Frontend code formatting check (Prettier) - uses: creyD/prettier_action@v4.3 - with: - dry: True - prettier_options: --config frontend/.prettierrc --ignore-path frontend/.prettierignore --check frontend/**/*.ts + run: npm install prettier@~3.1.0 && npm run format:check + working-directory: ./frontend \ No newline at end of file diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index f0ebfa2f7..ddb903625 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -7,12 +7,15 @@ on: - hotfixes - develop - workshop + - dev-suivi-eolien pull_request: branches: - master - hotfixes - develop - workshop + - workshop + - dev-suivi-eolien jobs: build: @@ -23,10 +26,6 @@ jobs: fail-fast: false matrix: include: - - name: "Debian 10" - python-version: "3.7" - postgres-version: 11 - postgis-version: 2.5 - name: "Debian 11" python-version: "3.9" postgres-version: 13 @@ -91,6 +90,10 @@ jobs: -e ..[tests] \ -r requirements-dev.txt working-directory: ./dependencies/GeoNature/backend + - name: Install monitoring module + run: | + python -m pip install -e . + working-directory: . - name: Install database run: | geonature db upgrade geonature@head -x local-srid=2154 diff --git a/README.md b/README.md index 6550dbbab..88d15d5a5 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,92 @@ -# Module GeoNature générique de suivis - -## Module générique de gestion des données de protocoles de type suivis - -Ce module permet de gérer de façon générique des données de protocoles "simples" articulés en 3 niveaux : des sites (nom, type, localisation) dans lesquels on fait des visites (dates, observateurs) dans lesquelles on peut faire des observations (espèces). - -Ces 3 niveaux peuvent être complétés des données spécifiques à chaque protocole, qui sont stockées dynamiquement dans la base de données sous forme de jsonb. - -![Liste des sites du protocole de test](docs/images/apercu.png) - -Le module permet de générer des sous-modules (stockés dans la table `gn_commons.t_modules`) pour chaque protocole de suivi. Ils s'appuient sur les champs fixes des 3 tables `gn_monitoring.t_base_sites`, `gn_monitoring.t_base_visits` et `gn_monitoring.t_observations` qui peuvent chacunes être étendues avec des champs spécifiques et dynamiques stockés dans des champs de type `JSONB`. - -Les champs spécifiques de chaque sous-module sont définis dans des fichiers de configuration au format json. - -Pour chaque sous-module, correspondant à un protocole spécifique de suivi, il est ainsi possible d'ajouter dynamiquement des champs de différent type (liste, nomenclature, booléen, date, radio, observateurs, texte, taxonomie...). Ceux-ci peuvent être obligatoires ou non, affichés ou non et avoir des valeurs par défaut. Les listes d'observateurs et d'espèces peuvent aussi être définies au niveau de chaque sous-module, en fonction du contexte du protocole de suivi. - -Des fonctions SQL ainsi qu'une vue définie pour chaque protocole permettent d'alimenter automatiquement la synthèse de GeoNature à partir des données saisies dans chaque sous-module. - -![MCD du schema gn_monitoring](docs/images/2020-06-MCD-monitoring.jpg) +# Module Monitoring ## Sommaire -* [Installation](#installation) +* [Présentation du module](#présentation) +* [Installation du module](#installation-du-module) +* [Installation d'un sous-module](#installation-dun-sous-module) +* [Champs spécifiques d'un sous-module](#configuration-des-champs-sp%C3%A9cifiques-dun-sous-module) +* [Gestion des sites et groupes de site](#gestion-des-sites) +* [Permissions](#permissions) +* [Base de données](#base-de-données) * [Gestion de la synthèse](docs/synthese.md) * [Documentation technique](docs/documentation_technique.md) -* [Création d'un sous-module](docs/sous_module.md) -* [Mise à jour du module](docs/MAJ.md) * [Liste des commandes](docs/commandes.md) -* [Permissions](#permissions) -## Installation +## Présentation + +Ce module permet de générer de façon générique des interfaces de saisie correspondant à des protocoles de suivi. +Par "suivi", on entend un protocole dont le point d'entrée est un site géographique, sur lequel on va revenir régulièrement effectuer des visites. Il se distingue par sa structure du module "Occtax" dont l'objectif est de faire de la saisie de données opportunistes ou d'inventaire (sans revenir régulièrement sur le même site de suivi). -### Pré-requis +Le module est articulé autour de 3 concepts : -* Avoir GeoNature installé dans une version compatible avec celle de la version du module. +- les sites : l'objet géographique de suivi (qui peuvent être regroupés par groupes de sites) +- les visites : une visite est effectuée sur un site (date, observateurs) +- les observations : observations faites durant la visite (espèces) -### Récupération du dépôt +Les 3 niveaux que sont les sites, les visites et les observations sont fournis avec un tronc commun (les champs génériques) qui peuvent être complétés par des champs spécifiques à chaque protocole. Ces champs spécifiques sont définis par des fichiers de configuration JSON. +Pour chaque sous-module, correspondant à un protocole spécifique de suivi, il est ainsi possible d'ajouter dynamiquement des champs de différents types (liste, nomenclature, booléen, date, radio, observateurs, texte, taxonomie...). Ceux-ci peuvent être obligatoires ou non, affichés ou non et avoir des valeurs par défaut (voir doc détaillée : [Création d'un sous-module](docs/sous_module.md)). -Pour récupérer le code source du module, vous pouvez le télécharger ou le cloner. +![Liste des sites du protocole de test](docs/images/apercu.png) -#### Téléchargement -```sh +## Installation du module + +- Téléchargez le module dans ``/home//``, en remplacant ``X.Y.Z`` par la version souhaitée + +```bash +cd wget https://github.com/PnX-SI/gn_module_monitoring/archive/X.Y.Z.zip unzip X.Y.Z.zip rm X.Y.Z.zip -mv gn_module_monitoring-X.Y.Z gn_module_monitoring ``` -Avec `X`, `Y`, `Z` correspondant à la version souhaitée. -#### Clonage du dépôt +- Renommez le répertoire du module -```sh -git clone https://github.com/PnX-SI/gn_module_monitoring.git +```bash +mv ~/gn_module_monitoring-X.Y.Z ~/gn_module_monitoring ``` -### Installation du module +- Lancez l'installation du module -```sh +```bash source ~/geonature/backend/venv/bin/activate geonature install-gn-module ~/gn_module_monitoring MONITORINGS sudo systemctl restart geonature deactivate ``` -Créer le dossier suivant dans le dossier `media` de GeoNature +Il vous faut désormais attribuer des permissions aux groupes ou utilisateurs que vous souhaitez, pour qu'ils puissent accéder et utiliser le module (voir https://docs.geonature.fr/admin-manual.html#gestion-des-droits). Si besoin une commande permet d'attribuer automatiquement toutes les permissions dans tous les modules à un groupe ou utilisateur administrateur. -```sh -mkdir ~/geonature/backend/media/monitorings -``` +### Mise à jour du module -Il vous faut désormais attribuer des permissions aux groupes ou utilisateurs que vous souhaitez, pour qu'ils puissent accéder et utiliser le module (voir https://docs.geonature.fr/admin-manual.html#gestion-des-droits). Si besoin une commande permet d'attribuer automatiquement toutes les permissions dans tous les modules à un groupe ou utilisateur administrateur. +Pour mettre à jour le modue Monitoring, suivre la documentation de [mise à jour d'un module GeoNature](https://docs.geonature.fr/installation.html#mise-a-jour-du-module) + +### Configuration générale du module -### Installation d'un sous-module +Le fichier de configuration `monitorings_config.toml.example` peut être modifié puis copié à la racine du dossier de config de GeoNature : `~/geonature/config`. -#### Récupérer le répertoire de configuration d'un sous-module de suivi +3 éléments sont paramétrables : -Par exemple le sous-module `test` présent dans le repertoire `contrib/test` du module de suivi. +- `TITLE_MODULE` : Titre présent sur la page d'accueil du module Monitoring +- `DESCRIPTION_MODULE` : Description du module Monitoring également présent sur la page d'accueil +- `CODE_OBSERVERS_LIST` : Code de la liste d'observateurs qui est utilisée par défaut -#### Activer le venv de GeoNature +## Installation d'un sous-module + +### Récupérer le répertoire de configuration d'un sous-module de suivi + +Par exemple le sous-module `test` présent dans le repertoire `contrib/test` du module Monitoring. + +### Activer le venv de GeoNature ```sh source ~/geonature/backend/venv/bin/activate ``` -#### Copie du dossier de configuration +### Copie du dossier de configuration -Créer un dossier pour référencer les configurations des sous-modules dans GeoNature (`geonature/backend/media/monitorings`) : +Si il n'existe pas déjà, créer un dossier pour stocker les configurations des sous-modules dans GeoNature (`geonature/backend/media/monitorings`) : ```sh mkdir geonature/backend/media/monitorings @@ -103,7 +104,7 @@ Exemple pour le module "test" : ln -s ~/gn_module_monitoring/contrib/test ~/geonature/backend/media/monitorings/test ``` -#### Lancer la commande d'installation du sous-module +### Lancer la commande d'installation du sous-module ```sh geonature monitorings install @@ -131,9 +132,9 @@ Modules installés : - module2: Module 2 (Deuxième exemple de module) ``` -Il vous faut désormais attribuer des permissions aux groupes ou utilisateurs que vous souhaitez, pour qu'ils puissent accéder et utiliser le sous-module (voir https://docs.geonature.fr/admin-manual.html#gestion-des-droits). Si besoin une commande permet d'attribuer automatiquement toutes les permissions dans tous les modules à un groupe ou utilisateur administrateur. +Il vous faut désormais attribuer des permissions aux groupes ou utilisateurs que vous souhaitez, pour qu'ils puissent accéder et utiliser le sous-module (voir ). Si besoin une commande permet d'attribuer automatiquement toutes les permissions dans tous les modules à un groupe ou utilisateur administrateur. -### Configurer le sous-module +### Configurer un sous-module #### Dans le menu de droite de GeoNature, cliquer sur le module "Monitorings" @@ -145,53 +146,109 @@ Vous êtes désormais sur la page du sous-module. Un message apparaît pour vous #### Cliquez sur le bouton `Éditer` -Le formulaire d'édition du sous-module s'affiche et vous pouvez choisir les variables suivantes : +Le formulaire d'édition du sous-module s'affiche et vous pouvez renseigner les variables suivantes : * Jeux de données *(obligatoire)* : - * Un module peut concerner plusieurs jeux de données, le choix sera ensuite proposé au niveau de chaque visite. + * Un sous-module peut concerner un ou plusieurs jeux de données, le choix sera ensuite proposé au niveau de chaque visite. * Liste des observateurs *(obligatoire)* : - * La liste d'observateurs définit l'ensemble des observateurs possibles pour le module (et de descripteurs de site). - * Cette liste peut être définie dans l'application `UsersHub`. + * La liste d'observateurs définit l'ensemble des observateurs possibles pour le sous-module (et de descripteurs de site). + * Cette liste peut être gérée dans l'application `UsersHub`. * Liste des taxons *(obligatoire selon le module)* : - * Cette liste définit l'ensemble des taxons concernés par ce module. Elle est gérée dans l'application `TaxHub`. -* Activer la synthèse *(non obligatoire, désactivée par défaut)* ? - * Si on décide d'intégrer les données du sous-module dans la synthèse de GeoNature. + * Cette liste définit l'ensemble des taxons concernés par ce sous-module. Elle est gérée dans l'application `TaxHub`. +* Activer la synchro synthèse *(non obligatoire, désactivée par défaut)* ? + * Si on décide d'intégrer automatiquement les données du sous-module dans la synthèse de GeoNature. * Affichage des taxons *(obligatoire)* ? * Définit comment sont affichés les taxons dans le module : * `lb_nom` : Nom latin, * `nom_vern,lb_nom` : Nom vernaculaire par defaut s'il existe, sinon nom latin. -* Afficher dans le menu ? *(non obligatoire, non affiché par défaut)* : - * On peut décider que le sous-module soit accessible directement depuis le menu de gauche de GeoNature. +* Afficher dans le menu latéral ? *(non obligatoire, non affiché par défaut)* : + * On peut décider que le sous-module soit accessible directement depuis le menu latéral de GeoNature. * `active_frontend` +* Types de sites : + * Permet d'associer des sites (créés dans le gestionnaire de sites) à un sous-module. Tous les sites dont le type est défini ici remonteront dans le module ([voir documentation sur le gestionnaire de sites (#gestionnaire-de-sites)) * Options spécifiques du sous-module : * Un sous-module peut présenter des options qui lui sont propres et définies dans les paramètres spécifiques du sous-module. -### Exemples de sous-modules +## Configuration des champs spécifiques d'un sous-module + +Maintenant que le sous-module est installé, vous pouvez configurer ses champs spécifiques pour le faire correspondre à votre protocole de suivi. +La documentation détaillée de la configuration des champs additionnels est ici : [Configuration des champs d'un sous module](docs/sous_module.md) -D'autres exemples de sous-modules sont disponibles sur le dépôt - : +Des exemples de sous-modules sont disponibles sur le dépôt : -* Protocole de suivi des oedicnèmes, +* Protocole de suivi des oedicnèmes * Protocole de suivi des mâles chanteurs de l'espèce chevêche - d'Athena; + d'Athena * Protocole Suivi Temporel des Oiseaux de Montagne (STOM) * Autres... +## Gestion des sites + +Chaque sous-module permet de créer ses propres sites et groupes de sites. Cependant certains sites peuvent faire l'objet de plusieurs protocoles de suivi, c'est pouquoi le module Monitoring offre la possibilité de créer des sites et des groupes de sites globalement dans le **gestionnaire de site** et de les utiliser dans plusieurs sous-modules. + +![Page d'accueil accès aux sites](docs/images/page_accueil_monitoring_acces_sites.png) + +Dans le gestionnaire de sites, il est possible de créer, éditer, supprimer des sites et des groupes de sites de manière indépendante à la gestion de sous-modules. Il est également possible de saisir directement des visites et des observations en rattachant les visites au sous-module que l'on souhaite. + +> [!IMPORTANT] +> **Associer un site à un sous-module** +> +> Plutôt que d'associer un à un les sites à des sous-modules, l'association entre un site et un sous-module se fait via la notion de **types de sites**. Une type de sites est un concept permettant de regrouper des sites qui font l'objet de plusieurs protocoles et qui partagent potentiellement une série de descripteurs communs. +> +> Un "point d'écoute" qui va par exemple faire l'objet de plusieurs protocoles ornithologiques (STOC, oiseaux migrateurs etc...) peut être définit comme un type de sites. +> +> Lors de la configuration d'un sous-module (depuis l'interface), on doit l'associer à un ou des types de site. Tous les sites créés via le gestionnaire de sites dont le type correspond à celui défini au niveau du sous-module, remonteront dans la liste des sites du sous-module. +> +> **Associer un groupe de sites à un sous-module** +> +> L'association entre un groupe de sites et un sous-module se fait individuellement. Lorsque l'on crée un groupe de site dans le gestionnaire de sites, on l'associe directement à un ou plusieurs sous-modules. + + +**Définir des champs spécifique à un type de site** + +Il est possible de définir des champs spécifiques communs à chaque type de sites. +Contrairement aux configurations des sous-modules, celle-ci ne se fait pas dans un fichier JSON, mais dans le module Admin de GeoNature (rubrique Monitoring / Types de sites). + +![admin type de sites](docs/images/admin_type_site.png) + +La syntaxe est la même que pour la création de champs d'un sous-module (voir [Création d'un sous-module](docs/sous_module.md)). La clé `specific` permettant de configurer les champs et la clé `display_properties` de définir les champs à afficher sur les fiches info des sites. + ## Permissions -Les permissions ne sont implémentées que partiellement. La notion de portée (mes données, les données de mon organisme, toutes les données) n'est pas prise en compte. Si un utilisateur a le droit de réaliser une action sur un type d'objet, il peut le faire sur l'ensemble des données. +- Une permission définit si l'on peut accéder au module Monitoring (R sur Monitoring) +- Des permissions définissent si on peut accéder au gestionnaire de sites et y créer, modifier, supprimer des sites et groupes de sites +- Des permissions définissent si on peut gérer les types de sites dans le module Admin + +Les permissions des sous-modules sont définies au niveau de chaque sous-module pour chaque type d'objet (sous-module, groupes de sites, sites, visites, observations) : + +- `MONITORINGS_MODULES` - R : permet à l'utilisateur d'accéder au sous-module, de le voir dans la liste des sous-modules +- `MONITORINGS_MODULES` - U : action administrateur qui permet de configurer le sous-module et de synchroniser la synthèse +- `MONITORINGS_MODULES` - E : action qui permet aux utilisateurs d'exporter les données (si configuré au niveau du sous-module) +- `MONITORINGS_GRP_SITES` - CRUD : action de lire, créer, modifier, supprimer un groupe de sites +- `MONITORINGS_SITES` - CRUD : action de lire, créer, modifier, supprimer un site +- `MONITORINGS_VISITES` - CRUD : action de lire, créer, modifier, supprimer les visites, observations, observations détails + +Les permissions des sous-modules peuvent être limitées avec une notion de portée : 'Mes données' ou 'Les données de mon organisme' : -La gestion des permissions pour les rôles (utilisateur ou groupe) se réalise au niveau de l'interface d'administration des permissions de GeoNature. +- Pour les groupes de sites, la portée s'appuie sur son id_digitizer (et son organisme si la portée est de niveau 2) +- Site : id_digitizer ou id_inventor du site +- Visite : id_digitiser ou observers de la visite +- Observation : id_digitizer de l'observation -Les permissions sont définis pour chaque type d'objet (modules, groupes de sites, sites, visites et observations) : - * MONITORINGS_MODULES - R : permet a l'utilisateur d'accéder au module, de le voir dans la liste des modules - * MONITORINGS_MODULES - U : action administrateur qui permet de configurer le module et de synchroniser la synthèse - * MONITORINGS_MODULES - E : action qui permet aux utilisateurs d'exporter les données (si défini par le module) - * MONITORINGS_GRP_SITES - CRUD : action de lire, créer, modifier, supprimer un groupe de site - * MONITORINGS_SITES - CRUD : action de lire, créer, modifier, supprimer un site - * MONITORINGS_VISITES - CRUD : action de lire, créer, modifier, supprimer les visites, observations, observations détails +Si vous modifiez la configuration d'un sous-module en y ajoutant des objets (ajout du niveau groupe de sites par exemple), il est possible de mettre à jour les permissions disponibles pour ce sous-module en utilisant la commande `update_module_available_permissions`. -Par défaut, dès qu'un utilisateur a un droit supérieur à 0 pour une action (c-a-d aucune portée) il peut réaliser cette action. +## Base de données +Le module permet de générer des sous-modules (stockés dans la table `gn_commons.t_modules`) pour chaque protocole de suivi. Ils s'appuient sur les champs fixes des 3 tables `gn_monitoring.t_base_sites`, `gn_monitoring.t_base_visits` et `gn_monitoring.t_observations` qui peuvent chacunes être étendues avec des champs spécifiques et dynamiques stockés dans des champs de type `JSONB`. + +Des fonctions SQL ainsi qu'une vue définie pour chaque protocole permettent d'alimenter automatiquement la synthèse de GeoNature à partir des données saisies dans chaque sous-module. -Il est possible de mettre à jour les permissions disponibles pour un module en utilisant la commande `update_module_available_permissions` +Les sites et groupes de sites peuvent être associés à plusieurs protocoles (sous-modules). + +![MCD du schema gn_monitoring](docs/images/2023-10-MCD_schema_monitoring.png) + +## Autres + +* [Gestion de la synthèse](docs/synthese.md) +* [Documentation technique](docs/documentation_technique.md) +* [Liste des commandes](docs/commandes.md) diff --git a/backend/gn_module_monitoring/blueprint.py b/backend/gn_module_monitoring/blueprint.py index 2e56e3ce7..1e5678b54 100644 --- a/backend/gn_module_monitoring/blueprint.py +++ b/backend/gn_module_monitoring/blueprint.py @@ -4,7 +4,12 @@ """ from flask import Blueprint, current_app -from .command.cmd import commands + +from geonature.utils.env import DB +from geonature.core.admin.admin import admin as flask_admin + +from gn_module_monitoring.monitoring.admin import BibTypeSiteView +from gn_module_monitoring.command.cmd import commands blueprint = Blueprint( "monitorings", __name__, template_folder=current_app.config["MEDIA_FOLDER"] + "/monitorings" @@ -14,3 +19,5 @@ blueprint.cli.short_help = "Commandes pour l" "administration du module MONITORINGS" for cmd in commands: blueprint.cli.add_command(cmd) + +flask_admin.add_view(BibTypeSiteView(DB.session, name="Types de site", category="Monitorings")) diff --git a/backend/gn_module_monitoring/command/cmd.py b/backend/gn_module_monitoring/command/cmd.py index 0d40a3df8..cc7b9e7d1 100644 --- a/backend/gn_module_monitoring/command/cmd.py +++ b/backend/gn_module_monitoring/command/cmd.py @@ -1,51 +1,47 @@ -import os -from pydoc import cli import click from pathlib import Path from flask.cli import with_appcontext -from sqlalchemy.sql import text +from sqlalchemy.sql import text, select -from geonature.utils.env import DB, BACKEND_DIR +from geonature.utils.env import DB from geonature.core.gn_synthese.models import TSources from geonature.core.gn_synthese.utils.process import import_from_table from geonature.core.gn_commons.models import TModules +from gn_module_monitoring.config.repositories import get_config +from gn_module_monitoring.config.utils import monitoring_module_config_path +from gn_module_monitoring.monitoring.models import TMonitoringModules +from gn_module_monitoring.modules.repositories import get_simple_module -from ..monitoring.models import TMonitoringModules -from ..config.repositories import get_config -from ..config.utils import json_from_file, monitoring_module_config_path -from ..modules.repositories import get_module, get_simple_module - -from .utils import ( - process_export_csv, +from gn_module_monitoring.command.utils import ( process_available_permissions, remove_monitoring_module, add_nomenclature, available_modules, installed_modules, + process_sql_files, ) -@click.command("process_all") +@click.command("process_sql") @click.argument("module_code", type=str, required=False, default="") @with_appcontext -def cmd_process_all(module_code): +def cmd_process_sql(module_code): """ Met à jour les paramètres de configuration pour un module + Fichiers sql synthese et export """ - # process export csv - process_export_csv(module_code) + if module_code: + modules = [module_code] + else: + modules = [module["module_code"] for module in installed_modules()] - -@click.command("process_export_csv") -@click.argument("module_code", type=str, required=False, default="") -@with_appcontext -def cmd_process_export_csv(module_code): - """ - Met à jour les fichiers pour les exports pdf - """ - process_export_csv(module_code) + for module in modules: + # process Synthese + process_sql_files(dir=None, module_code=module, depth=1) + # process Exports + process_sql_files(dir="exports/csv", module_code=module, depth=None, allowed_files=None) @click.command("install") @@ -98,8 +94,10 @@ def cmd_install_monitoring_module(module_code): except Exception: pass - # process export csv - process_export_csv(module_code) + # process Synthese + process_sql_files(dir=None, module_code=module_code, depth=1) + # process Exports + process_sql_files(dir=None, module_code=module_code, depth=None, allowed_files=None) config = get_config(module_code, force=True) @@ -141,23 +139,6 @@ def cmd_install_monitoring_module(module_code): process_available_permissions(module_code, session=DB.session) DB.session.commit() - #  run specific sql - if (module_config_dir_path / "synthese.sql").exists: - click.secho("Execution du script synthese.sql") - sql_script = module_config_dir_path / "synthese.sql" - try: - DB.engine.execute( - text( - open(sql_script, "r") - .read() - .replace(":'module_code'", "'{}'".format(module_code)) - .replace(":module_code", "{}".format(module_code)) - ).execution_options(autocommit=True) - ) - except Exception as e: - print(e) - click.secho("Erreur dans le script synthese.sql", fg="red") - # insert nomenclature add_nomenclature(module_code) @@ -195,14 +176,13 @@ def cmd_process_available_permission_module(module_code): module_code ([string]): code du sous module """ - if module_code: - process_available_permissions(module_code, session=DB.session) - DB.session.commit() - return + modules = [module_code] + else: + modules = [module["module_code"] for module in installed_modules()] - for module in installed_modules(): - process_available_permissions(module["module_code"], session=DB.session) + for module in modules: + process_available_permissions(module, session=DB.session) DB.session.commit() @@ -238,7 +218,9 @@ def synchronize_synthese(module_code, offset): Synchronise les données d'un module dans la synthese """ click.secho(f"Start synchronize data for module {module_code} ...", fg="green") - module = TModules.query.filter_by(module_code=module_code).one() + module = DB.session.execute( + select(TModules).where(TModules.module_code == module_code) + ).scalar_one() table_name = "v_synthese_{}".format(module_code) import_from_table( "gn_monitoring", @@ -251,11 +233,10 @@ def synchronize_synthese(module_code, offset): commands = [ - cmd_process_export_csv, cmd_install_monitoring_module, cmd_process_available_permission_module, cmd_remove_monitoring_module_cmd, cmd_add_module_nomenclature_cli, - cmd_process_all, + cmd_process_sql, synchronize_synthese, ] diff --git a/backend/gn_module_monitoring/command/utils.py b/backend/gn_module_monitoring/command/utils.py index efbb2198b..4d1c261a1 100644 --- a/backend/gn_module_monitoring/command/utils.py +++ b/backend/gn_module_monitoring/command/utils.py @@ -1,22 +1,33 @@ import os + from pathlib import Path from flask import current_app -from sqlalchemy import and_, text +from sqlalchemy import and_, text, delete, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm.exc import NoResultFound - -from geonature.utils.env import DB, BACKEND_DIR -from geonature.core.gn_permissions.models import PermObject, PermissionAvailable, PermAction +from sqlalchemy.dialects.postgresql import insert as pg_insert + +from geonature.utils.env import DB +from geonature.core.gn_permissions.models import ( + PermObject, + PermissionAvailable, + PermAction, + cor_object_module, +) from geonature.core.gn_commons.models import TModules - +from geonature.core.gn_monitoring.models import BibTypeSite from pypnnomenclature.models import TNomenclatures, BibNomenclaturesTypes -from ..config.utils import json_from_file, monitoring_module_config_path, SUB_MODULE_CONFIG_DIR +from gn_module_monitoring.config.utils import ( + json_from_file, + monitoring_module_config_path, + SUB_MODULE_CONFIG_DIR, +) -from ..config.repositories import get_config +from gn_module_monitoring.config.repositories import get_config -from ..modules.repositories import get_module, get_source_by_code, get_modules +from gn_module_monitoring.modules.repositories import get_module, get_source_by_code, get_modules """ @@ -26,6 +37,19 @@ """ +FORBIDDEN_SQL_INSTRUCTION = [ + "INSERT ", + "DELETE ", + "UPDATE ", + "EXECUTE ", + "TRUNCATE ", + "ALTER ", + "GRANT ", + "COPY ", + "PERFORM ", + "CASCADE", +] + PERMISSION_LABEL = { "MONITORINGS_MODULES": {"label": "modules", "actions": ["R", "U", "E"]}, "MONITORINGS_GRP_SITES": {"label": "groupes de sites", "actions": ["C", "R", "U", "D"]}, @@ -42,48 +66,63 @@ } -def process_for_all_module(process_func): - """ - boucle sur les répertoire des module - et exécute la fonction pour chacun - (sauf generic) - """ - for module in get_modules(): - process_func(module.module_code) - return - - -def process_export_csv(module_code=None): - """ - fonction qui va chercher les fichier sql de exports/csv et qui les joue - """ - - if not module_code: - """ - pour tous les modules - """ - return process_for_all_module(process_export_csv) - - export_csv_dir = Path(monitoring_module_config_path(module_code)) / "exports/csv" - - if not export_csv_dir.is_dir(): +def process_sql_files( + dir=None, module_code=None, depth=1, allowed_files=["export.sql", "synthese.sql"] +): + sql_dir = Path(monitoring_module_config_path(module_code)) + if dir: + sql_dir = sql_dir / "exports/csv" + if not sql_dir.is_dir(): return - for root, dirs, files in os.walk(export_csv_dir, followlinks=True): + if not allowed_files: + allowed_files = [] + count_depth = 0 + for root, dirs, files in os.walk(sql_dir, followlinks=True): + count_depth = count_depth + 1 for f in files: if not f.endswith(".sql"): continue - + if not f in allowed_files and allowed_files: + continue + # Vérification commandes non autorisée try: - DB.engine.execute( - text(open(Path(root) / f, "r").read()) - .execution_options(autocommit=True) - .bindparams(module_code=module_code) + execute_sql_file(root, f, module_code, FORBIDDEN_SQL_INSTRUCTION) + print("{} - exécution du fichier : {}".format(module_code, f)) + except Exception as e: + print(e) + + # Limite profondeur de la recherche dans les répertoires + if depth: + if count_depth >= depth: + break + + +def execute_sql_file(dir, file, module_code, forbidden_instruction=[]): + """ + Execution d'un fichier sql dans la base de donnée + dir : nom du répertoire + file : nom du fichier à éxécuter + module_code : code du module + forbidden_instruction : liste d'instructions sql qui sont proscrites du fichier. + + """ + sql_content = Path(Path(dir) / file).read_text() + for sql_cmd in forbidden_instruction: + if sql_cmd.lower() in sql_content.lower(): + raise Exception( + "erreur dans le script {} instruction sql non autorisée {}".format( + module_code, file, sql_cmd ) - print("{} - export csv file : {}".format(module_code, f)) + ) - except Exception as e: - print("{} - export csv erreur dans le script {} : {}".format(module_code, f, e)) + try: + DB.engine.execute( + text(sql_content), + module_code=module_code, + ) + except Exception as e: + raise Exception("{} - erreur dans le script {} : {}".format(module_code, file, e)) def process_available_permissions(module_code, session): @@ -119,43 +158,50 @@ def insert_module_available_permissions(module_code, perm_object_code, session): print(f"L'object {perm_object_code} n'est pas traité") try: - module = session.query(TModules).filter_by(module_code=module_code).one() + module = session.scalars(select(TModules).where(TModules.module_code == module_code)).one() except NoResultFound: print(f"Le module {module_code} n'est pas présent") return try: - perm_object = session.query(PermObject).filter_by(code_object=perm_object_code).one() + perm_object = session.execute( + select(PermObject).where(PermObject.code_object == perm_object_code) + ).scalar_one_or_none() except NoResultFound: print(f"L'object de permission {perm_object_code} n'est pas présent") return - txt_cor_object_module = f""" - INSERT INTO gn_permissions.cor_object_module( - id_module, - id_object - ) - VALUES({module.id_module}, {perm_object.id_object}) - ON CONFLICT DO NOTHING - """ - session.execute(txt_cor_object_module) + stmt = ( + pg_insert(cor_object_module) + .values(id_module=module.id_module, id_object=perm_object.id_object) + .on_conflict_do_nothing() + ) + session.execute(stmt) + session.commit() # Création d'une permission disponible pour chaque action object_actions = PERMISSION_LABEL.get(perm_object_code)["actions"] for action in object_actions: - permaction = session.query(PermAction).filter_by(code_action=action).one() + permaction = session.execute( + select(PermAction).where(PermAction.code_action == action) + ).scalar_one() try: - perm = ( - session.query(PermissionAvailable) - .filter_by(module=module, object=perm_object, action=permaction) - .one() - ) + perm = session.execute( + select(PermissionAvailable).where( + PermissionAvailable.module == module, + PermissionAvailable.object == perm_object, + PermissionAvailable.action == permaction, + ) + ).scalar_one() except NoResultFound: + label = f"{ACTION_LABEL[action]} {object_label}" + if action == "E" and perm_object.code_object == "MONITORINGS_MODULES": + label = "Export les données du module" perm = PermissionAvailable( module=module, object=perm_object, action=permaction, - label=f"{ACTION_LABEL[action]} {object_label}", + label=label, scope_filter=True, ) session.add(perm) @@ -171,13 +217,14 @@ def remove_monitoring_module(module_code): # remove module in db try: # suppression des permissions disponibles pour ce module - txt = f"DELETE FROM gn_permissions.t_permissions_available WHERE id_module = {module.id_module}" - DB.engine.execution_options(autocommit=True).execute(txt) + # txt = f"DELETE FROM gn_permissions.t_permissions_available WHERE id_module = {module.id_module}" + stmt = delete(PermissionAvailable).where(PermissionAvailable.id_module == module.id_module) + + DB.session.execute(stmt) - # HACK pour le moment suppresion avec un sql direct - # Car il y a un soucis de delete cascade dans les modèles sqlalchemy - txt = f"""DELETE FROM gn_commons.t_modules WHERE id_module ={module.id_module}""" - DB.engine.execution_options(autocommit=True).execute(txt) + stmt = delete(TModules).where(TModules.id_module == module.id_module) + DB.session.execute(stmt) + DB.session.commit() except IntegrityError: print("Impossible de supprimer le module car il y a des données associées") return @@ -212,19 +259,20 @@ def add_nomenclature(module_code): return for data in nomenclature.get("types", []): - nomenclature_type = None - try: - nomenclature_type = ( - DB.session.query(BibNomenclaturesTypes) - .filter(data.get("mnemonique") == BibNomenclaturesTypes.mnemonique) - .one() + nomenclature_type = DB.session.execute( + select(BibNomenclaturesTypes).where( + data.get("mnemonique") == BibNomenclaturesTypes.mnemonique ) - - except Exception: - pass + ).scalar_one_or_none() if nomenclature_type: - print("no insert type", nomenclature_type) + action = "already exist" + print( + "nomenclature type {} - {} - {}".format( + nomenclature_type.mnemonique, nomenclature_type.label_default, action + ) + ) + continue data["label_fr"] = data.get("label_fr") or data["label_default"] @@ -235,66 +283,77 @@ def add_nomenclature(module_code): nomenclature_type = BibNomenclaturesTypes(**data) DB.session.add(nomenclature_type) DB.session.commit() + action = "added" + print( + "nomenclature type {} - {} - {}".format( + nomenclature_type.mnemonique, nomenclature_type.label_default, action + ) + ) for data in nomenclature["nomenclatures"]: - nomenclature = None - try: - nomenclature = ( - DB.session.query(TNomenclatures) - .join( - BibNomenclaturesTypes, BibNomenclaturesTypes.id_type == TNomenclatures.id_type - ) - .filter( - and_( - data.get("cd_nomenclature") == TNomenclatures.cd_nomenclature, - data.get("type") == BibNomenclaturesTypes.mnemonique, - ) - ) - .one() - ) + insert_update_nomenclature(data) - except Exception as e: - pass - if nomenclature: - # TODO make update - print( - "nomenclature {} - {} already exist".format( - nomenclature.cd_nomenclature, nomenclature.label_default - ) - ) - continue +def insert_update_nomenclature(data): - id_type = None + # Get Id type + id_type = DB.session.execute( + select(BibNomenclaturesTypes.id_type).where( + BibNomenclaturesTypes.mnemonique == data["type"] + ) + ).scalar_one_or_none() - try: - id_type = ( - DB.session.query(BibNomenclaturesTypes.id_type) - .filter(BibNomenclaturesTypes.mnemonique == data["type"]) - .one() - )[0] - except Exception: - pass - - if not id_type: - print( - 'probleme de type avec mnemonique="{}" pour la nomenclature {}'.format( - data["type"], data - ) + if not id_type: + print( + 'probleme de type avec mnemonique="{}" pour la nomenclature {}'.format( + data["type"], data ) - continue + ) + return - data["label_fr"] = data.get("label_fr") or data["label_default"] - data["definition_fr"] = data.get("definition_fr") or data["definition_default"] - data["source"] = data.get("source") or "monitoring" - data["statut"] = data.get("statut") or "Validation en cours" - data["active"] = True - data["id_type"] = id_type - data.pop("type") + # Get nomenclature if exist + action = "updated" + nomenclature = DB.session.execute( + select(TNomenclatures) + .join(BibNomenclaturesTypes, BibNomenclaturesTypes.id_type == TNomenclatures.id_type) + .where( + and_( + data.get("cd_nomenclature") == TNomenclatures.cd_nomenclature, + data.get("type") == BibNomenclaturesTypes.mnemonique, + ) + ) + ).scalar_one_or_none() - nomenclature = TNomenclatures(**data) - DB.session.add(nomenclature) - DB.session.commit() + # If not create new one + if not nomenclature: + nomenclature = TNomenclatures() + action = "added" + + data["label_fr"] = data.get("label_fr") or data["label_default"] + data["definition_fr"] = data.get("definition_fr") or data["definition_default"] + data["source"] = data.get("source") or "monitoring" + data["statut"] = data.get("statut") or "Validation en cours" + data["active"] = True + data["id_type"] = id_type + + for key, value in data.items(): + if hasattr(nomenclature, key): + setattr(nomenclature, key, value) + + DB.session.add(nomenclature) + DB.session.commit() + if data["type"] == "TYPE_SITE": + existing_bib_type_site = DB.session.get(BibTypeSite, nomenclature.id_nomenclature) + if not existing_bib_type_site: + bib_type_site = BibTypeSite(id_nomenclature_type_site=nomenclature.id_nomenclature) + DB.session.add(bib_type_site) + DB.session.commit() + + print( + "nomenclature {} - {} - {}".format( + nomenclature.cd_nomenclature, nomenclature.label_default, action + ) + ) def installed_modules(session=None): diff --git a/backend/gn_module_monitoring/conf_schema_toml.py b/backend/gn_module_monitoring/conf_schema_toml.py index 3c27b1173..bdc2c10b2 100644 --- a/backend/gn_module_monitoring/conf_schema_toml.py +++ b/backend/gn_module_monitoring/conf_schema_toml.py @@ -4,7 +4,7 @@ Fichier à ne pas modifier. Paramètres surcouchables dans config/config_gn_module.tml """ -from marshmallow import Schema, fields, validates_schema, ValidationError +from marshmallow import Schema, fields # Permissions associés à chaque objet monitoring @@ -19,6 +19,10 @@ class GnModuleSchemaConf(Schema): + DESCRIPTION_MODULE = fields.String(load_default="") + TITLE_MODULE = fields.String(load_default="Module monitoring") + CODE_OBSERVERS_LIST = fields.String(load_default="obsocctax") + PERMISSION_LEVEL = fields.Dict( keys=fields.Str(), values=fields.Str(), load_default=PERMISSION_LEVEL_DEFAULT ) diff --git a/backend/gn_module_monitoring/config/data_utils.py b/backend/gn_module_monitoring/config/data_utils.py deleted file mode 100644 index 792ee525f..000000000 --- a/backend/gn_module_monitoring/config/data_utils.py +++ /dev/null @@ -1,65 +0,0 @@ -# from pypnusershub.db.models import User -from apptax.taxonomie.models import Taxref, CorNomListe - -from pypnnomenclature.models import TNomenclatures, BibNomenclaturesTypes -from geonature.utils.env import DB -from sqlalchemy import and_ - -from .utils import config_from_files_customized - - -def config_data(module_code): - return config_from_files_customized("data", module_code) - - -def get_data_utils(module_code): - return { - "nomenclature": get_nomenclature(module_code), - "taxonomy": get_taxonomy(module_code), - "users": {}, - } - - -def get_nomenclature(module_code): - nomenclature_types = config_data(module_code).get("nomenclature") - - if not nomenclature_types: - return {} - - q = ( - DB.session.query(TNomenclatures) - .join(BibNomenclaturesTypes, BibNomenclaturesTypes.id_type == TNomenclatures.id_type) - .filter(BibNomenclaturesTypes.mnemonique.in_(nomenclature_types)) - .all() - ) - - return {d.id_nomenclature: d.as_dict() for d in q} - - -def get_taxonomy(module_code): - id_list = config_data(module_code)["taxonomy"].get("id_list") - taxonomy = get_taxonomy_from_id_list(id_list) - - return taxonomy - - -def get_taxonomy_from_id_list(id_list): - if not id_list: - return {} - - id_list - q = ( - DB.session.query(Taxref) - .join( - CorNomListe, and_(CorNomListe.id_liste == id_list, CorNomListe.id_nom == Taxref.cd_nom) - ) - .join() - .all() - ) - - return {(d.cd_nom): (d.nom_complet) for d in q} - - -def get_users(module_code): - return {} - pass diff --git a/backend/gn_module_monitoring/config/generic/config.json b/backend/gn_module_monitoring/config/generic/config.json index f2dafb345..76365477f 100644 --- a/backend/gn_module_monitoring/config/generic/config.json +++ b/backend/gn_module_monitoring/config/generic/config.json @@ -1,25 +1,28 @@ { "tree": { "module": { - "site": { - "visit": { - "observation": null + "sites_group": { + "site": { + "visit": { + "observation": null + } } } } }, - "synthese" : "__MODULE.B_SYNTHESE", + "synthese": "__MODULE.B_SYNTHESE", "default_display_field_names": { "user": "nom_complet", "nomenclature": "label_fr", "dataset": "dataset_name", "observer_list": "nom_liste", - "taxonomy" : "__MODULE.TAXONOMY_DISPLAY_FIELD_NAME", + "taxonomy": "__MODULE.TAXONOMY_DISPLAY_FIELD_NAME", "taxonomy_list": "nom_liste", "sites_group": "sites_group_name", "habitat": "lb_hab_fr", "area": "area_name", "municipality": "nom_com_dept", - "site": "base_site_name" + "site": "base_site_name", + "module": "module_label" } -} +} \ No newline at end of file diff --git a/backend/gn_module_monitoring/config/generic/module.json b/backend/gn_module_monitoring/config/generic/module.json index 352a5dacf..64875e008 100644 --- a/backend/gn_module_monitoring/config/generic/module.json +++ b/backend/gn_module_monitoring/config/generic/module.json @@ -15,7 +15,6 @@ "attribut_label": "ID", "hidden": true }, - "module_code": { "type_widget": "text", "attribut_label": "Code", @@ -69,7 +68,8 @@ "application": "GeoNature", "required": true, "type_util": "observer_list", - "definition": "Liste des observateurs. À gérer dans UsersHub." + "definition": "Liste des observateurs. À gérer dans UsersHub.", + "designStyle": "bootstrap" }, "id_list_taxonomy": { @@ -83,7 +83,8 @@ "required": true, "type_util": "taxonomy_list", "data_path": "data", - "definition": "Liste des taxons. À gérer dans TaxHub." + "definition": "Liste des taxons. À gérer dans TaxHub.", + "designStyle": "bootstrap" }, "b_synthese": { @@ -112,7 +113,8 @@ "value": "lb_nom" } ], - "required": true + "required": true, + "designStyle": "bootstrap" }, "active_frontend": { @@ -120,6 +122,22 @@ "attribut_label": "Afficher dans le menu ?", "definition": "Afficher le module dans le menu de GeoNature. (Recharger la page pour voir les modifications)." }, + + "types_site": { + "type_widget": "datalist", + "attribut_label": "Types de sites", + "type_util": "types_site", + "keyValue": "id_nomenclature_type_site", + "keyLabel": "label", + "multiple": true, + "api" : "__MONITORINGS_PATH/sites/types", + "application": "GeoNature", + "required": true, + "data_path": "items", + "definition": "Permet de paramétrer la compatibilité de ce module avec les types de sites", + "designStyle": "bootstrap" + }, + "medias": { "type_widget": "medias", "attribut_label": "Médias", diff --git a/backend/gn_module_monitoring/config/generic/observation.json b/backend/gn_module_monitoring/config/generic/observation.json index 5788fd4a9..c15997d73 100644 --- a/backend/gn_module_monitoring/config/generic/observation.json +++ b/backend/gn_module_monitoring/config/generic/observation.json @@ -17,6 +17,13 @@ "attribut_label": "Id visite", "hidden": true }, + "id_digitiser": { + "type_widget": "text", + "attribut_label": "Numérisateur", + "required": true, + "hidden": true, + "type_util": "user" + }, "cd_nom": { "type_widget": "taxonomy", "attribut_label": "Espèce", diff --git a/backend/gn_module_monitoring/config/generic/site.json b/backend/gn_module_monitoring/config/generic/site.json index f9c7f44b0..4f0a52aed 100644 --- a/backend/gn_module_monitoring/config/generic/site.json +++ b/backend/gn_module_monitoring/config/generic/site.json @@ -6,28 +6,32 @@ "genre": "M", "geom_field_name": "geom", "uuid_field_name": "uuid_base_site", - "geometry_type": "Point", + "geometry_type": ["Point", "LineString", "Polygon"], "display_properties": [ "base_site_name", "base_site_code", "base_site_description", - "id_nomenclature_type_site", "id_inventor", "first_use_date", "last_visit", "nb_visits", "altitude_min", - "altitude_max" + "altitude_max", + "types_site" ], "display_list": [ "base_site_name", "base_site_code", - "id_nomenclature_type_site", "last_visit", - "nb_visits" + "id_inventor", + "nb_visits", + "types_site" ], "sorts": [ - {"prop": "last_visit", "dir": "desc"} + { + "prop": "last_visit", + "dir": "desc" + } ], "generic": { "id_base_site": { @@ -35,11 +39,6 @@ "attribut_label": "Id site", "hidden": true }, - "id_module": { - "type_widget": "text", - "attribut_label": "ID Module", - "hidden": true - }, "base_site_code": { "type_widget": "text", "attribut_label": "Code", @@ -53,32 +52,21 @@ "base_site_description": { "type_widget": "textarea", "attribut_label": "Description" - }, - "id_nomenclature_type_site": { - "type_widget": "text", - "attribut_label": "Type site", - "type_util": "nomenclature", - "value": { - "code_nomenclature_type": "TYPE_SITE" - }, - "required": true }, "id_inventor": { - "type_widget": "datalist", - "attribut_label": "Descripteur", - "api": "users/menu/__MODULE.ID_LIST_OBSERVER", - "application": "GeoNature", - "keyValue": "id_role", - "keyLabel": "nom_complet", + "type_widget": "observers", + "attribut_label": "Observateur", "type_util": "user", - "required": true - }, + "code_list": "CODE_OBSERVERS_LIST", + "required": true, + "multi_select": false + }, "id_digitiser": { "type_widget": "text", - "attribut_label": "Numérisateur", + "attribut_label": "Digitiser", + "type_util": "user", "required": true, - "hidden": true, - "type_util": "user" + "hidden": true }, "first_use_date": { "type_widget": "date", @@ -103,10 +91,28 @@ "altitude_min": { "type_widget": "integer", "attribut_label": "Altitude (min)" - }, - "altitude_max": { + }, + "altitude_max": { "type_widget": "integer", "attribut_label": "Altitude (max)" - } + }, + "types_site": { + "type_widget": "datalist", + "attribut_label": "Type(s) de site", + "type_util": "types_site", + "keyValue": "id_nomenclature_type_site", + "keyLabel": "label", + "multiple": true, + "api": "__MONITORINGS_PATH/modules/__MODULE.MODULE_CODE/types_sites", + "application": "GeoNature", + "required": true, + "nullDefault": true, + "definition": "Permet de n'avoir que les types de site lié au module", + "designStyle": "bootstrap" + }, + "id_sites_group": { + "hidden": true, + "required": false + } } } diff --git a/backend/gn_module_monitoring/config/generic/sites_group.json b/backend/gn_module_monitoring/config/generic/sites_group.json index 68bdf820d..2f6b87fb1 100644 --- a/backend/gn_module_monitoring/config/generic/sites_group.json +++ b/backend/gn_module_monitoring/config/generic/sites_group.json @@ -6,9 +6,12 @@ "label_list": "Groupes de sites", "genre": "M", "uuid_field_name": "uuid_sites_group", + "geom_field_name": "geom", + "geometry_type": "Polygon", "display_properties": [ "sites_group_name", "sites_group_code", + "modules", "nb_sites", "nb_visits", "comments" @@ -22,11 +25,6 @@ "attribut_label": "Id site", "hidden": true }, - "id_module": { - "type_widget": "text", - "attribut_label": "ID Module", - "hidden": true - }, "sites_group_name": { "type_widget": "text", "attribut_label": "Nom", @@ -58,6 +56,39 @@ "type_widget": "medias", "attribut_label": "Médias", "schema_dot_table": "gn_monitoring.t_sites_groups" + }, + "altitude_min": { + "type_widget": "integer", + "attribut_label": "Altitude (min)" + }, + "altitude_max": { + "type_widget": "integer", + "attribut_label": "Altitude (max)" + }, + "id_digitiser": { + "type_widget": "text", + "attribut_label": "Digitiser", + "type_util": "user", + "required": true, + "hidden": true + }, + "modules": { + "type_widget": "datalist", + "type_util": "module", + "attribut_label": "Modules", + "keyValue": "id_module", + "keyLabel": "module_label", + "multiple": true, + "default": [ + { + "id_module": "__MODULE.ID_MODULE" + } + ], + "api" : "monitorings/modules", + "application": "GeoNature", + "required": true, + "designStyle": "bootstrap", + "definition": "Permet de contrôler dans quel(s) module(s) les groupes de site remonteront" } - } + } } diff --git a/backend/gn_module_monitoring/config/generic/visit.json b/backend/gn_module_monitoring/config/generic/visit.json index 68afdd982..f57c7ce14 100644 --- a/backend/gn_module_monitoring/config/generic/visit.json +++ b/backend/gn_module_monitoring/config/generic/visit.json @@ -22,9 +22,7 @@ "nb_observations" ], - "sorts": [ - {"prop": "visit_date_min", "dir": "desc"} - ], + "sorts": [{ "prop": "visit_date_min", "dir": "desc" }], "generic": { "id_base_visit": { "type_widget": "text", @@ -50,9 +48,15 @@ "keyLabel": "nom_complet", "type_util": "user", "multiple": true, + "hidden": false, "required": true }, - + "observers_txt": { + "type_widget": "observers-text", + "attribut_label": "Observateur(s) (extérieur)", + "required": false, + "hidden": true + }, "id_digitiser": { "type_widget": "text", "attribut_label": "Digitiser", diff --git a/backend/gn_module_monitoring/config/repositories.py b/backend/gn_module_monitoring/config/repositories.py index f196ebede..705ea1cc9 100644 --- a/backend/gn_module_monitoring/config/repositories.py +++ b/backend/gn_module_monitoring/config/repositories.py @@ -2,14 +2,14 @@ module de gestion de la configuarion des protocoles de suivi """ -import json import os + from flask import current_app -from .utils import ( + +from gn_module_monitoring.config.utils import ( customize_config, config_from_files, - get_directory_last_modif, - get_base_last_modif, + json_config_from_db, json_config_from_file, get_id_table_location, process_config_display, @@ -19,7 +19,7 @@ get_data_preload, monitoring_module_config_path, ) - +from gn_module_monitoring.utils.utils import dict_deep_update # pour stocker la config dans current_app.config config_cache_name = "MONITORINGS_CONFIG" @@ -33,16 +33,25 @@ def get_config_objects(module_code, config, tree=None, parent_type=None): # initial tree tree = config["tree"] + if "module" in config["tree"]: + is_sites_group_child = "sites_group" in list(dict.fromkeys(config["tree"]["module"])) + for object_type in tree: # config object if not object_type in config: - config[object_type] = config_object_from_files(module_code, object_type) + if object_type == "site": + config[object_type] = config_object_from_files( + module_code, object_type, is_sites_group_child + ) + else: + config[object_type] = config_object_from_files(module_code, object_type) # tree children_types = tree[object_type] and list(tree[object_type].keys()) or [] if not "children_types" in config[object_type]: config[object_type]["children_types"] = [] + config[object_type]["children_types"] += children_types config[object_type]["children_types"] = list( dict.fromkeys(config[object_type]["children_types"]) @@ -80,18 +89,54 @@ def get_config_objects(module_code, config, tree=None, parent_type=None): get_config_objects(module_code, config, tree[object_type], object_type) -def config_object_from_files(module_code, object_type): +def config_object_from_files(module_code, object_type, custom=None, is_sites_group_child=False): """ recupere la configuration d'un object de type pour le module """ generic_config_object = json_config_from_file("generic", object_type) specific_config_object = ( - {} if module_code == "generic" else json_config_from_file(module_code, object_type) + {"specific": {}} + if module_code == "generic" + else json_config_from_file(module_code, object_type) ) + db_config_object = {"specific": {}} + + if object_type == "site": + db_config_object = json_config_from_db(module_code) + # Mise a jour des configurations de façon récursive + dict_deep_update(specific_config_object["specific"], db_config_object["specific"]) + + elif object_type == "module": + db_config_object = json_config_from_db(module_code) + specific_config_object["types_site"] = db_config_object["types_site"] + db_config_object = {"specific": {}} + + if module_code == "generic" and object_type == "site": + generic_config_object["generic"]["types_site"] = { + "type_widget": "datalist", + "attribut_label": "Type(s) de site", + "type_util": "types_site", + "keyValue": "id_nomenclature_type_site", + "keyLabel": "label", + "multiple": True, + "api": "monitorings/modules/generic/types_sites", + "application": "GeoNature", + "required": True, + "nullDefault": True, + "designStyle": "bootstrap", + "definition": "Permet de n'avoir que les types de site lié au module", + } + specific_config_object["specific"]["id_sites_group"] = {"required": False, "hidden": False} + + # if object_type == "site" and custom is not None: + # if "specific" in custom and "specific" in specific_config_object: + # for key in custom["specific"]: + # if key not in specific_config_object["specific"]: + # specific_config_object["specific"][key] = custom["specific"][key] config_object = generic_config_object + config_object.update(db_config_object) config_object.update(specific_config_object) - return config_object @@ -103,8 +148,9 @@ def get_config(module_code=None, force=False): et si aucun fichier du dossier de configuration n'a été modifié depuis le dernier appel de cette fonction alors la configuration est récupéré depuis current_app.config sinon la config est recupérée depuis les fichiers du dossier de configuration et stockée dans current_app.config - """ + if module_code == "MONITORINGS": + module_code = "generic" module_code = module_code if module_code else "generic" module_confg_dir_path = monitoring_module_config_path(module_code) @@ -121,7 +167,6 @@ def get_config(module_code=None, force=False): return config module = get_monitoring_module(module_code) - # derniere modification # fichiers # file_last_modif = get_directory_last_modif(monitoring_config_path()) @@ -135,10 +180,10 @@ def get_config(module_code=None, force=False): config = config_from_files("config", module_code) get_config_objects(module_code, config) + # customize config + config["custom"] = {} if module: - custom = {} - config["custom"] = {} for field_name in [ "module_code", "id_list_observer", @@ -152,17 +197,27 @@ def get_config(module_code=None, force=False): config["custom"][var_name] = getattr(module, field_name) config["module"][field_name] = getattr(module, field_name) - config["custom"]["__MONITORINGS_PATH"] = get_monitorings_path() - - config["default_display_field_names"].update(config.get("display_field_names", {})) - config["display_field_names"] = config["default_display_field_names"] - - # Remplacement des variables __MODULE.XXX - # par les valeurs spécifiées en base - customize_config(config, config["custom"]) + config["custom"]["__MODULE.TYPES_SITE"] = [ + type_site.as_dict() for type_site in module.types_site + ] + config["default_display_field_names"].update(config.get("display_field_names", {})) + config["display_field_names"] = config["default_display_field_names"] + + # preload data # TODO auto from schemas && config recup tax users nomenclatures etc.... + config["data"] = get_data_preload(config, module) + else: + # Si module est généric + config["custom"]["CODE_OBSERVERS_LIST"] = current_app.config["MONITORINGS"].get( + "CODE_OBSERVERS_LIST", {} + ) + config["custom"]["__MODULE.MODULE_CODE"] = "generic" + config["custom"]["__MODULE.ID_MODULE"] = None + config["custom"]["__MODULE.B_SYNTHESE"] = False - # preload data # TODO auto from schemas && config recup tax users nomenclatures etc.... - config["data"] = get_data_preload(config, module) + config["custom"]["__MONITORINGS_PATH"] = get_monitorings_path() + # Remplacement des variables __MODULE.XXX + # par les valeurs spécifiées en base + customize_config(config, config["custom"]) # mise en cache dans current_app.config[config_cache_name][module_code] if not current_app.config.get(config_cache_name, {}): @@ -172,73 +227,10 @@ def get_config(module_code=None, force=False): return config -def config_param(module_code, object_type, param_name): - """ - revoie un parametre de la configuration des objets - - :param module_code: reference le module concerne - :param object_type: le type d'object (site, visit, obervation) - :param param_name: le parametre voulu (id_field_name, label) - :type module_code: str - :type object_type: str - :type param_name: str - :return: valeur du paramètre requis - :rtype: str - - :Exemple: +# def get_config_from_backend(module_code=None, force=False): - config_param('oedic', 'site', 'id_field_name') - renverra 'id_base_site' - - config_param('oedic', 'site', 'label') - renverra 'Site' - - """ - - config = get_config(module_code) - - return config[object_type].get(param_name) - - -def config_schema(module_code, object_type, type_schema="all"): - """ - renvoie une liste d'éléments de configuration de formulaire - - pour type_schema: - 'generic' : renvoie le schema générique - 'specific' : renvoie le schema spécifique - 'all': par defaut renvoie tout le schema - - Un élément est un dictionaire de type - { - "attribut_name": "id_base_site", - "Label": "Id du site", - "type_widget": "integer", - "required": "true", - } - - :param module_code: reference le module concerne - :param object_type: le type d'object (site, visit, obervation) - :param type_schema: le type de schema requis ('all', 'generic', 'specific') - :type module_code: str - :type object_type: str - :type type_schema: str, optional - :return: tableau d'élément de configuration de formulaire - :rtype: list - """ - # recuperation de la configuration - config = get_config(module_code) - - if type_schema in ["generic", "specific"]: - return config[object_type][type_schema] - - # renvoie le schema complet si type_schema == 'all' ou par defaut - schema = dict(config[object_type]["generic"]) - schema.update(config[object_type]["specific"]) - - return schema - - -def get_config_frontend(module_code=None, force=True): - config = dict(get_config(module_code, force)) - return config +# module_code = 'generic' +# #TODO: voir la sortie de cette fonction +# config = config_from_backend('config', module_code) +# #TODO: voir également à quoi sert cette fonction +# get_config_objects(module_code, config) diff --git a/backend/gn_module_monitoring/config/utils.py b/backend/gn_module_monitoring/config/utils.py index 8b1f572ce..aafc2e1bc 100644 --- a/backend/gn_module_monitoring/config/utils.py +++ b/backend/gn_module_monitoring/config/utils.py @@ -1,15 +1,19 @@ import os, datetime, time import importlib -from pathlib import Path import json +from pathlib import Path -from sqlalchemy import and_ +from sqlalchemy import and_, select +from sqlalchemy.orm.exc import NoResultFound -from geonature.core.gn_commons.models import BibTablesLocation, TModules -from geonature.utils.errors import GeoNatureError from geonature.utils.env import DB +from geonature.utils.errors import GeoNatureError from geonature.utils.config import config as gn_config -from ..monitoring.models import TMonitoringModules +from geonature.core.gn_commons.models import BibTablesLocation, TModules + +from gn_module_monitoring.monitoring.models import TMonitoringModules +from gn_module_monitoring.modules.repositories import get_module +from gn_module_monitoring.utils.routes import query_all_types_site_from_module_id SUB_MODULE_CONFIG_DIR = Path(gn_config["MEDIA_FOLDER"]) / "monitorings/" @@ -21,8 +25,10 @@ "keyLabel": "sites_group_name", "api": "__MONITORINGS_PATH/list/__MODULE.MODULE_CODE/sites_group?id_module=__MODULE.ID_MODULE&fields=id_sites_group&fields=sites_group_name", "application": "GeoNature", + "designStyle": "bootstrap", } + def monitoring_module_config_path(module_code): return SUB_MODULE_CONFIG_DIR / module_code @@ -35,21 +41,16 @@ def get_monitoring_module(module_code): if module_code == "generic": return None - res = ( - DB.session.query(TMonitoringModules) - .filter(TMonitoringModules.module_code == module_code) - .all() - ) - - return res[0] if len(res) else None + return DB.session.execute( + select(TMonitoringModules).where(TMonitoringModules.module_code == module_code) + ).scalar_one_or_none() def get_monitorings_path(): - return ( - DB.session.query(TModules.module_path) - .filter(TModules.module_code == "MONITORINGS") - .one()[0] - ) + module = DB.session.execute( + select(TModules.module_path).where(TModules.module_code == "MONITORINGS") + ).scalar_one() + return module def get_base_last_modif(module): @@ -88,16 +89,14 @@ def get_id_table_location(object_type): id_table_location = None try: - id_table_location = ( - DB.session.query(BibTablesLocation.id_table_location) - .filter( + id_table_location = DB.session.execute( + select(BibTablesLocation.id_table_location).where( and_( BibTablesLocation.schema_name == schema_name, BibTablesLocation.table_name == table_name, ) ) - .one() - )[0] + ).scalar_one() except Exception as e: print(schema_name, table_name, e) pass @@ -148,6 +147,42 @@ def json_config_from_file(module_code, type_config): return json_from_file(file_path, {}) +def json_config_from_db(module_code): + site_type_config = {"types_site": {}, "specific": {}} + if module_code == "generic": + # Si generic récupération de tous les types de sites + types = query_all_types_site_from_module_id(0) + else: + try: + module = get_module("module_code", module_code) + except NoResultFound: + return site_type_config + types = query_all_types_site_from_module_id(module.id_module) + + for t in types: + fields = [] + + # Configuration des champs + if "specific" in (t.config or {}): + site_type_config["specific"].update(t.config["specific"]) + fields = [k for k in t.config["specific"]] + + # Liste des champs à afficher + display_properties = list(fields) + if "display_properties" in (t.config or {}): + display_properties = [ + key for key in t.config.get("display_properties") if key in fields + ] + display_properties + [key for key in fields if not key in display_properties] + + site_type_config["types_site"][t.id_nomenclature_type_site] = { + "display_properties": display_properties, + "name": t.nomenclature.label_default, + } + + return site_type_config + + def config_from_files(config_type, module_code): generic_config_custom = json_config_from_file("generic", config_type) specific_config_custom = ( diff --git a/backend/gn_module_monitoring/migrations/0defdace9997_add_site_object_for_monitoring_module.py b/backend/gn_module_monitoring/migrations/0defdace9997_add_site_object_for_monitoring_module.py new file mode 100644 index 000000000..9f00b3ece --- /dev/null +++ b/backend/gn_module_monitoring/migrations/0defdace9997_add_site_object_for_monitoring_module.py @@ -0,0 +1,38 @@ +"""add site object for monitoring module + +Revision ID: 0defdace9997 +Revises: 7fbcdd93626a +Create Date: 2024-06-17 15:45:58.888781 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "0defdace9997" +down_revision = "7fbcdd93626a" +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute( + """ + INSERT INTO gn_permissions.cor_object_module (id_object ,id_module) + VALUES + ((select id_object from gn_permissions.t_objects where code_object = 'MONITORINGS_SITES'), + (select id_module from gn_commons.t_modules where module_code = 'MONITORINGS')); + """ + ) + + +def downgrade(): + op.execute( + """ + DELETE FROM gn_permissions.cor_object_module + WHERE id_object = (select id_object from gn_permissions.t_objects where code_object = 'MONITORINGS_SITES') + AND id_module = (select id_module from gn_commons.t_modules where module_code = 'MONITORINGS'); + """ + ) diff --git a/backend/gn_module_monitoring/migrations/2003e18f248a_add_type_to_gn_modules.py b/backend/gn_module_monitoring/migrations/2003e18f248a_add_type_to_gn_modules.py index 6407bd6f0..3c2124bfb 100644 --- a/backend/gn_module_monitoring/migrations/2003e18f248a_add_type_to_gn_modules.py +++ b/backend/gn_module_monitoring/migrations/2003e18f248a_add_type_to_gn_modules.py @@ -5,6 +5,7 @@ Create Date: 2022-12-19 14:01:42.559701 """ + from alembic import op import sqlalchemy as sa @@ -29,7 +30,7 @@ def upgrade(): def downgrade(): op.execute( """ - UPDATE gn_commons.t_modules AS tm SET type=NULL + UPDATE gn_commons.t_modules AS tm SET type='' FROM gn_monitoring.t_module_complements AS tmc WHERE tm.id_module = tmc.id_module; """ diff --git a/backend/gn_module_monitoring/migrations/34253c8fa9b9_declare_available_types_sites_.py b/backend/gn_module_monitoring/migrations/34253c8fa9b9_declare_available_types_sites_.py new file mode 100644 index 000000000..c64e0aba9 --- /dev/null +++ b/backend/gn_module_monitoring/migrations/34253c8fa9b9_declare_available_types_sites_.py @@ -0,0 +1,152 @@ +"""declare available types sites permissions + +Revision ID: 34253c8fa9b9 +Revises: 0defdace9997 +Create Date: 2024-07-11 16:44:23.736722 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "34253c8fa9b9" +down_revision = "0defdace9997" +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute( + """ + INSERT INTO + gn_permissions.t_objects( + code_object, + description_object + ) + VALUES + ('TYPES_SITES','Types de sites à associer aux protocoles du module MONITORINGS') + """ + ) + op.execute( + """ + INSERT INTO + gn_permissions.t_permissions_available ( + id_module, + id_object, + id_action, + label, + scope_filter + ) + SELECT + m.id_module, + o.id_object, + a.id_action, + v.label, + v.scope_filter + FROM + ( + VALUES + ('MONITORINGS', 'TYPES_SITES', 'R', False, 'Accéder aux types de site'), + ('MONITORINGS', 'TYPES_SITES', 'C', False, 'Créer des types de site'), + ('MONITORINGS', 'TYPES_SITES', 'U', False, 'Modifier des types de site'), + ('MONITORINGS', 'TYPES_SITES', 'D', False, 'Supprimer des types de site'), + ('MONITORINGS', 'MONITORINGS_SITES', 'R', True, 'Accéder aux sites'), + ('MONITORINGS', 'MONITORINGS_SITES', 'C', True, 'Créer des sites'), + ('MONITORINGS', 'MONITORINGS_SITES', 'U', True, 'Modifier des sites'), + ('MONITORINGS', 'MONITORINGS_SITES', 'D', True, 'Supprimer des sites'), + ('MONITORINGS', 'MONITORINGS_GRP_SITES', 'R', True, 'Accéder aux groupes de sites'), + ('MONITORINGS', 'MONITORINGS_GRP_SITES', 'C', True, 'Créer des groupes de sites'), + ('MONITORINGS', 'MONITORINGS_GRP_SITES', 'U', True, 'Modifier des groupes de sites'), + ('MONITORINGS', 'MONITORINGS_GRP_SITES', 'D', True, 'Supprimer des groupes de sites') + ) AS v (module_code, object_code, action_code, scope_filter, label) + JOIN + gn_commons.t_modules m ON m.module_code = v.module_code + JOIN + gn_permissions.t_objects o ON o.code_object = v.object_code + JOIN + gn_permissions.bib_actions a ON a.code_action = v.action_code + """ + ) + op.execute( + """ + WITH bad_permissions AS ( + SELECT + p.id_permission + FROM + gn_permissions.t_permissions p + JOIN gn_commons.t_modules m + USING (id_module) + WHERE + m.module_code = 'MONITORINGS' + EXCEPT + SELECT + p.id_permission + FROM + gn_permissions.t_permissions p + JOIN gn_permissions.t_permissions_available pa ON + (p.id_module = pa.id_module + AND p.id_object = pa.id_object + AND p.id_action = pa.id_action) + ) + DELETE + FROM + gn_permissions.t_permissions p + USING bad_permissions bp + WHERE + bp.id_permission = p.id_permission; + """ + ) + + op.execute( + """ + INSERT INTO gn_permissions.t_objects (code_object, description_object) + VALUES + ('MONITORINGS_MODULES', 'Permissions sur les modules') + ON CONFLICT DO NOTHING + ; + """ + ) + + +def downgrade(): + op.execute( + """ + DELETE FROM + gn_permissions.t_permissions_available pa + USING + gn_commons.t_modules m + WHERE + pa.id_module = m.id_module + AND + module_code = 'MONITORINGS' + AND + pa.id_object IN ( + SELECT to2.id_object + FROM gn_permissions.t_objects to2 + WHERE code_object IN ('TYPES_SITES', 'MONITORINGS_SITES', 'MONITORINGS_GRP_SITES') + ) + """ + ) + + op.execute( + """ + DELETE FROM + gn_permissions.t_permissions p + USING gn_permissions.t_objects o + WHERE + p.id_object = o.id_object + AND o.code_object IN ('TYPES_SITES', 'MONITORINGS_SITES', 'MONITORINGS_GRP_SITES', 'MONITORINGS_MODULES') + ; + """ + ) + + op.execute( + """ + DELETE FROM + gn_permissions.t_objects + WHERE code_object IN ('TYPES_SITES', 'MONITORINGS_SITES', 'MONITORINGS_GRP_SITES', 'MONITORINGS_MODULES') + ; + """ + ) diff --git a/backend/gn_module_monitoring/migrations/362cf9d504ec_create_monitoring_schema.py b/backend/gn_module_monitoring/migrations/362cf9d504ec_create_monitoring_schema.py index 73f1332e8..149396674 100644 --- a/backend/gn_module_monitoring/migrations/362cf9d504ec_create_monitoring_schema.py +++ b/backend/gn_module_monitoring/migrations/362cf9d504ec_create_monitoring_schema.py @@ -5,6 +5,7 @@ Create Date: 2021-03-29 18:38:24.512562 """ + import importlib from alembic import op @@ -33,15 +34,39 @@ def upgrade(): def downgrade(): - op.drop_table("t_module_complements", monitorings_schema) - op.drop_table("t_observation_complements", monitorings_schema) - op.drop_table("t_observation_details", monitorings_schema) - op.drop_table("t_observations", monitorings_schema) - op.drop_table("t_site_complements", monitorings_schema) - op.drop_table("t_sites_groups", monitorings_schema) - op.drop_table("t_visit_complements", monitorings_schema) + op.drop_table("t_module_complements", schema=monitorings_schema) + op.drop_table("t_observation_complements", schema=monitorings_schema) + op.drop_table("t_observation_details", schema=monitorings_schema) + op.drop_table("t_site_complements", schema=monitorings_schema) + op.drop_table("t_sites_groups", schema=monitorings_schema) + op.drop_table("t_visit_complements", schema=monitorings_schema) # Remove all GNM related objects + + op.execute( + """ + DELETE FROM + gn_permissions.t_permissions p + USING gn_permissions.t_objects o + WHERE + p.id_object = o.id_object + AND o.code_object like 'GNM_%' + ; + """ + ) + + op.execute( + """ + DELETE FROM + gn_permissions.t_permissions_available p + USING gn_permissions.t_objects o + WHERE + p.id_object = o.id_object + AND o.code_object like 'GNM_%' + ; + """ + ) + statement = sa.delete(TObjects).where(TObjects.code_object.like("GNM_%")) op.execute(statement) @@ -49,9 +74,7 @@ def downgrade(): statement = sa.delete(BibTablesLocation).where( and_( BibTablesLocation.schema_name == monitorings_schema, - BibTablesLocation.table_name.in_( - ("t_module_complements", "t_observations", "t_sites_groups") - ), + BibTablesLocation.table_name.in_(("t_module_complements", "t_sites_groups")), ) ) op.execute(statement) diff --git a/backend/gn_module_monitoring/migrations/3ffeea74a9dd_rename_gnm__to_monitorings_.py b/backend/gn_module_monitoring/migrations/3ffeea74a9dd_rename_gnm__to_monitorings_.py index 8686a6834..9d694e7b7 100644 --- a/backend/gn_module_monitoring/migrations/3ffeea74a9dd_rename_gnm__to_monitorings_.py +++ b/backend/gn_module_monitoring/migrations/3ffeea74a9dd_rename_gnm__to_monitorings_.py @@ -1,10 +1,11 @@ """Rename GNM_ to MONITORINGS_ Revision ID: 3ffeea74a9dd -Revises: fc90d31c677f +Revises: a5498a5f6022 Create Date: 2023-10-02 12:00:30.382163 """ + from alembic import op import sqlalchemy as sa diff --git a/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py b/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py new file mode 100644 index 000000000..bf117bb68 --- /dev/null +++ b/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py @@ -0,0 +1,82 @@ +"""remove_id_module_from_sites_complements + +Revision ID: 6673266fb79c +Revises: a5dce2633e4c +Create Date: 2022-12-13 16:00:00.512562 + +""" + +import sqlalchemy as sa +from alembic import op + +from gn_module_monitoring import MODULE_CODE + +# revision identifiers, used by Alembic. +revision = "6673266fb79c" +down_revision = "e2b66850b5ee" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" + + +def upgrade(): + # Transfert data to core_site_module table + statement = sa.text( + f""" + INSERT INTO {monitorings_schema}.cor_site_module (id_module, id_base_site) + SELECT tsc.id_module, tsc.id_base_site + FROM {monitorings_schema}.t_site_complements AS tsc + LEFT JOIN {monitorings_schema}.cor_site_module AS csm + ON tsc.id_base_site = csm.id_base_site + WHERE csm.id_base_site IS NULL; + """ + ) + op.execute(statement) + + # Drop column id_module + op.drop_column("t_site_complements", "id_module", schema=monitorings_schema) + + +def downgrade(): + op.add_column( + "t_site_complements", + sa.Column( + "id_module", + sa.Integer(), + sa.ForeignKey( + f"gn_commons.t_modules.id_module", + name="fk_t_site_complements_id_module", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=True, + ), + schema=monitorings_schema, + ) + # Cannot use orm here because need the model to be "downgraded" as well + # Need to set nullable True above for existing rows + # LIMITATION: Assume that current use is one site associated to one module + statement = sa.text( + f""" + WITH sm AS ( + SELECT min(id_module) AS first_id_module, id_base_site + FROM {monitorings_schema}.cor_site_module AS csm + GROUP BY id_base_site + ) + UPDATE {monitorings_schema}.t_site_complements sc + SET id_module = sm.first_id_module + FROM sm + WHERE sm.id_base_site = sc.id_base_site; + """ + ) + op.execute(statement) + + statement = sa.text( + f""" + DELETE FROM gn_monitoring.t_site_complements WHERE id_module IS NULL; + """ + ) + op.execute(statement) + + op.alter_column("t_site_complements", "id_module", nullable=False, schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/migrations/6a15625a0f4a_delete_object_all.py b/backend/gn_module_monitoring/migrations/6a15625a0f4a_delete_object_all.py index 16c575bb3..d4300d9dd 100644 --- a/backend/gn_module_monitoring/migrations/6a15625a0f4a_delete_object_all.py +++ b/backend/gn_module_monitoring/migrations/6a15625a0f4a_delete_object_all.py @@ -5,6 +5,7 @@ Create Date: 2023-10-02 13:53:05.682108 """ + from alembic import op import sqlalchemy as sa @@ -18,42 +19,7 @@ def upgrade(): # Suppression des permissions available de ALL pour les modules monitorings - op.execute( - """ - WITH to_del AS ( - SELECT tp.* - FROM gn_permissions.t_permissions_available AS tp - JOIN gn_commons.t_modules AS tm - ON tm.id_module = tp.id_module AND tm."type" = 'monitoring_module' - JOIN gn_permissions.t_objects AS o - ON o.id_object = tp.id_object AND code_object = 'ALL' - ) - DELETE FROM gn_permissions.t_permissions_available AS tp - USING to_del td - WHERE tp.id_module = td.id_module - AND tp.id_object = td.id_object - AND tp.id_action = td.id_action - AND tp."label" = td."label" - AND tp.scope_filter = td.scope_filter - AND tp.sensitivity_filter = td.sensitivity_filter; - """ - ) - - # Suppression des permissions de ALL pour les modules monitorings - op.execute( - """ - WITH to_del AS ( - SELECT DISTINCT tp.id_permission - FROM gn_permissions.t_permissions AS tp - JOIN gn_commons.t_modules AS tm - ON tm.id_module = tp.id_module AND tm."type" = 'monitoring_module' - JOIN gn_permissions.t_objects AS o - ON o.id_object = tp.id_object AND code_object = 'ALL' - ) - DELETE FROM gn_permissions.t_permissions AS tp - WHERE tp.id_permission IN (SELECT id_permission FROM to_del); - """ - ) + pass def downgrade(): diff --git a/backend/gn_module_monitoring/migrations/7fbcdd93626a_trigger_monitoring.py b/backend/gn_module_monitoring/migrations/7fbcdd93626a_trigger_monitoring.py new file mode 100644 index 000000000..2456a3a36 --- /dev/null +++ b/backend/gn_module_monitoring/migrations/7fbcdd93626a_trigger_monitoring.py @@ -0,0 +1,66 @@ +"""trigger drop synthese monitoring + +Revision ID: 7fbcdd93626a +Revises: f3413cccdfa8 +Create Date: 2024-01-11 17:25:05.135068 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "7fbcdd93626a" +down_revision = "f3413cccdfa8" +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute( + """ + + DROP TRIGGER IF EXISTS trg_delete_synthese_observations ON gn_monitoring.t_observations; + DROP FUNCTION IF EXISTS gn_synthese.fct_trg_delete_synthese_observations(); + + CREATE FUNCTION gn_synthese.fct_trg_delete_synthese_observations() RETURNS trigger AS $$ + BEGIN + --Suppression des données dans la synthèse + DELETE FROM gn_synthese.synthese WHERE unique_id_sinp = OLD.uuid_observation; + RETURN OLD; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER trg_delete_synthese_observations AFTER DELETE ON gn_monitoring.t_observations + FOR EACH ROW EXECUTE PROCEDURE gn_synthese.fct_trg_delete_synthese_observations(); + + + DROP TRIGGER IF EXISTS trg_delete_synthese_visits ON gn_monitoring.t_base_visits; + DROP FUNCTION IF EXISTS gn_synthese.fct_trg_delete_synthese_visits() CASCADE; + + CREATE FUNCTION gn_synthese.fct_trg_delete_synthese_visits() RETURNS trigger AS $$ + BEGIN + --Suppression des données dans la synthèse + DELETE FROM gn_synthese.synthese WHERE unique_id_sinp = OLD.uuid_base_visit; + RETURN OLD; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER trg_delete_synthese_visits AFTER DELETE ON gn_monitoring.t_base_visits + FOR EACH ROW EXECUTE PROCEDURE gn_synthese.fct_trg_delete_synthese_visits(); + + """ + ) + + +def downgrade(): + op.execute( + """ + DROP TRIGGER trg_delete_synthese_observations ON gn_monitoring.t_observations; + DROP FUNCTION gn_synthese.fct_trg_delete_synthese_observations(); + + DROP TRIGGER trg_delete_synthese_visits ON gn_monitoring.t_base_visits; + DROP FUNCTION gn_synthese.fct_trg_delete_synthese_visits(); + """ + ) diff --git a/backend/gn_module_monitoring/migrations/be30fb5c1a56_add_site_group_object_for_monitoring_.py b/backend/gn_module_monitoring/migrations/be30fb5c1a56_add_site_group_object_for_monitoring_.py new file mode 100644 index 000000000..8f69fd9f4 --- /dev/null +++ b/backend/gn_module_monitoring/migrations/be30fb5c1a56_add_site_group_object_for_monitoring_.py @@ -0,0 +1,38 @@ +"""add site group object for monitoring module + +Revision ID: be30fb5c1a56 +Revises: 34253c8fa9b9 +Create Date: 2024-07-12 14:42:28.611638 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "be30fb5c1a56" +down_revision = "34253c8fa9b9" +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute( + """ + INSERT INTO gn_permissions.cor_object_module (id_object ,id_module) + VALUES + ((select id_object from gn_permissions.t_objects where code_object = 'MONITORINGS_GRP_SITES'), + (select id_module from gn_commons.t_modules where module_code = 'MONITORINGS')); + """ + ) + + +def downgrade(): + op.execute( + """ + DELETE FROM gn_permissions.cor_object_module + WHERE id_object = (select id_object from gn_permissions.t_objects where code_object = 'MONITORINGS_GRP_SITES') + AND id_module = (select id_module from gn_commons.t_modules where module_code = 'MONITORINGS'); + """ + ) diff --git a/backend/gn_module_monitoring/migrations/c1528c94d350_upgrade_existing_permissions.py b/backend/gn_module_monitoring/migrations/c1528c94d350_upgrade_existing_permissions.py index 7b64602a5..327e22844 100644 --- a/backend/gn_module_monitoring/migrations/c1528c94d350_upgrade_existing_permissions.py +++ b/backend/gn_module_monitoring/migrations/c1528c94d350_upgrade_existing_permissions.py @@ -5,6 +5,7 @@ Create Date: 2023-10-02 12:09:53.695122 """ + from alembic import op import sqlalchemy as sa @@ -21,100 +22,7 @@ def upgrade(): - bind = op.get_bind() - session = sa.orm.Session(bind=bind) - - # Création des permissions disponibles pour chaque module - for module in installed_modules(session): - process_available_permissions(module["module_code"], session=session) - - # ######## - # Mise à jour des permissions existantes vers les sous objets - # Création des permission des sous-objets à partir des permissions contenus dans l'objet ALL - op.execute( - """ - WITH ap AS ( - SELECT o.code_object,o.id_object, tpa.id_module - FROM gn_permissions.t_permissions_available AS tpa - JOIN gn_permissions.t_objects AS o - ON o.id_object = tpa.id_object AND NOT code_object = 'ALL' - JOIN gn_commons.t_modules AS tm - ON tm.id_module = tpa.id_module AND tm."type" = 'monitoring_module' - JOIN gn_permissions.bib_actions AS ba - ON tpa.id_action = ba.id_action - WHERE NOT (code_object = 'MONITORINGS_MODULES' AND ba.code_action = 'U') - ), ep AS ( - SELECT id_role, id_action, tp.id_module , tp.id_object, scope_value, sensitivity_filter - FROM gn_permissions.t_permissions AS tp - JOIN gn_permissions.t_objects AS o - ON o.id_object = tp.id_object AND code_object = 'ALL' - JOIN gn_commons.t_modules AS tm - ON tm.id_module = tp.id_module AND tm."type" = 'monitoring_module' - ), new_p AS ( - SELECT DISTINCT ep.id_role, ep.id_action, ep.id_module, ap.id_object, ep.scope_value, ep.sensitivity_filter - FROM ep - JOIN ap - ON ep.id_module = ap.id_module - LEFT OUTER JOIN gn_permissions.t_permissions AS p - ON p.id_role = ep.id_role - AND p.id_action = ep.id_action - AND p.id_module = ep.id_module - AND p.id_object = ap.id_object - WHERE p.id_permission IS NULL - ) - INSERT INTO gn_permissions.t_permissions - (id_role, id_action, id_module, id_object, scope_value, sensitivity_filter) - SELECT id_role, id_action, id_module, id_object, scope_value, sensitivity_filter - FROM new_p; - """ - ) - - # Suppression des permissions available inutile - # on conserve POUR all - # R : accès au module - # U : modification des paramètres du module - # E : Exporter les données du module - op.execute( - """ - WITH to_del AS ( - SELECT tp.* - FROM gn_permissions.t_permissions_available AS tp - JOIN gn_commons.t_modules AS tm - ON tm.id_module = tp.id_module AND tm."type" = 'monitoring_module' - JOIN gn_permissions.t_objects AS o - ON o.id_object = tp.id_object AND code_object = 'ALL' - JOIN gn_permissions.bib_actions AS ba - ON tp.id_action = ba.id_action AND NOT ba.code_action IN ('R', 'E', 'U') - ) - DELETE FROM gn_permissions.t_permissions_available AS tp - USING to_del td - WHERE tp.id_module = td.id_module - AND tp.id_object = td.id_object - AND tp.id_action = td.id_action - AND tp."label" = td."label" - AND tp.scope_filter = td.scope_filter - AND tp.sensitivity_filter = td.sensitivity_filter; - """ - ) - - # Suppression des permissions qui ne sont pas dans les permissions available - op.execute( - """ - WITH to_del AS ( - SELECT tp.id_permission - FROM gn_permissions.t_permissions AS tp - JOIN gn_commons.t_modules AS tm - ON tm.id_module = tp.id_module AND tm."type" = 'monitoring_module' - LEFT OUTER JOIN gn_permissions.t_permissions_available AS ta - ON tp.id_action = ta.id_action - AND tp.id_module = ta.id_module - AND tp.id_object = ta.id_object - WHERE ta.id_module IS NULL - ) - DELETE FROM gn_permissions.t_permissions AS tp - WHERE tp.id_permission IN (SELECT id_permission FROM to_del); - """ - ) + pass def downgrade(): diff --git a/backend/gn_module_monitoring/migrations/data/monitoring.sql b/backend/gn_module_monitoring/migrations/data/monitoring.sql index 06d8dac85..e13b4d3f3 100644 --- a/backend/gn_module_monitoring/migrations/data/monitoring.sql +++ b/backend/gn_module_monitoring/migrations/data/monitoring.sql @@ -84,22 +84,7 @@ CREATE TABLE IF NOT EXISTS gn_monitoring.t_module_complements ( ON UPDATE CASCADE ON DELETE CASCADE ); - - CREATE TABLE IF NOT EXISTS gn_monitoring.t_observations ( - id_observation SERIAL NOT NULL, - id_base_visit INTEGER NOT NULL, - cd_nom INTEGER NOT NULL, - comments TEXT, - uuid_observation UUID DEFAULT uuid_generate_v4() NOT NULL, - - - CONSTRAINT pk_t_observations PRIMARY KEY (id_observation), - CONSTRAINT fk_t_observations_id_base_visit FOREIGN KEY (id_base_visit) - REFERENCES gn_monitoring.t_base_visits (id_base_visit) MATCH SIMPLE - ON UPDATE CASCADE ON DELETE CASCADE - ); - - + -- champs en complément de t_observation: relation 1-1 CREATE TABLE IF NOT EXISTS gn_monitoring.t_observation_complements ( @@ -133,7 +118,6 @@ CREATE TABLE IF NOT EXISTS gn_monitoring.t_module_complements ( INSERT INTO gn_commons.bib_tables_location(table_desc, schema_name, table_name, pk_field, uuid_field_name) VALUES ('Table centralisant les modules faisant l''objet de protocole de suivis', 'gn_monitoring', 't_module_complements', 'id_module', 'uuid_module_complement'), - ('Table centralisant les observations réalisées lors d''une visite sur un site', 'gn_monitoring', 't_observations', 'id_observation', 'uuid_observation'), ('Table centralisant les sites faisant l''objet de protocole de suivis', 'gn_monitoring', 't_base_sites', 'id_base_site', 'uuid_base_site'), ('Table centralisant les groupes de sites faisant l''objet de protocole de suivis', 'gn_monitoring', 't_sites_groups', 'id_sites_group', 'uuid_sites_group'), ('Table centralisant les visites réalisées sur un site', 'gn_monitoring', 't_base_visits', 'id_base_visit', 'uuid_base_visit') diff --git a/backend/gn_module_monitoring/migrations/e2b66850b5ee_add_digitiser_to_t_sites_groups.py b/backend/gn_module_monitoring/migrations/e2b66850b5ee_add_digitiser_to_t_sites_groups.py new file mode 100644 index 000000000..71b2da48c --- /dev/null +++ b/backend/gn_module_monitoring/migrations/e2b66850b5ee_add_digitiser_to_t_sites_groups.py @@ -0,0 +1,51 @@ +"""Add digitiser to t_sites_groups + +Revision ID: e2b66850b5ee +Revises: 6a15625a0f4a +Create Date: 2023-09-11 12:17:17.280948 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "e2b66850b5ee" +down_revision = "6a15625a0f4a" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" +table = "t_sites_groups" +column = "id_digitiser" + +foreign_schema = "utilisateurs" +table_foreign = "t_roles" +foreign_key = "id_role" + + +def upgrade(): + op.add_column( + table, + sa.Column( + column, + sa.Integer(), + sa.ForeignKey( + f"{foreign_schema}.{table_foreign}.{foreign_key}", + name=f"fk_{table}_{column}", + onupdate="CASCADE", + ), + ), + schema=monitorings_schema, + ) + + +def downgrade(): + statement = sa.text( + f""" + ALTER TABLE {monitorings_schema}.{table} DROP CONSTRAINT fk_{table}_{column}; + """ + ) + op.execute(statement) + op.drop_column(table, column, schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/migrations/e78003460441_correction_t_observation_detail.py b/backend/gn_module_monitoring/migrations/e78003460441_correction_t_observation_detail.py index a111c88f3..73028d107 100644 --- a/backend/gn_module_monitoring/migrations/e78003460441_correction_t_observation_detail.py +++ b/backend/gn_module_monitoring/migrations/e78003460441_correction_t_observation_detail.py @@ -5,10 +5,11 @@ Create Date: 2023-01-02 16:44:18.715547 """ + from alembic import op import sqlalchemy as sa - +# TODO: voir les discussions sur element patrinat pour voir si on a statué # revision identifiers, used by Alembic. revision = "e78003460441" down_revision = "2003e18f248a" diff --git a/backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py b/backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py new file mode 100644 index 000000000..837c3da6b --- /dev/null +++ b/backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py @@ -0,0 +1,85 @@ +"""remove_id_module_from_sites_groups + +Revision ID: f24adb481f54 +Revises: 6673266fb79c +Create Date: 2022-12-13 16:00:00.512562 + +""" + +import sqlalchemy as sa +from alembic import op + +from gn_module_monitoring import MODULE_CODE + +# revision identifiers, used by Alembic. +revision = "f24adb481f54" +down_revision = "6673266fb79c" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" + + +def upgrade(): + op.execute( + """ + CREATE TABLE gn_monitoring.cor_sites_group_module ( + id_sites_group int4 NOT NULL, + id_module int4 NOT NULL, + CONSTRAINT pk_cor_sites_group_module PRIMARY KEY (id_sites_group, id_module), + CONSTRAINT fk_cor_sites_group_module_id_sites_group FOREIGN KEY (id_sites_group) REFERENCES gn_monitoring.t_sites_groups(id_sites_group) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT fk_cor_sites_group_module_id_module FOREIGN KEY (id_module) REFERENCES gn_commons.t_modules(id_module) ON DELETE NO ACTION ON UPDATE CASCADE + ); + """ + ) + statement = sa.text( + f""" + INSERT INTO gn_monitoring.cor_sites_group_module + (id_sites_group, id_module) + SELECT id_sites_group, id_module + FROM gn_monitoring.t_sites_groups; + """ + ) + op.execute(statement) + op.drop_column("t_sites_groups", "id_module", schema=monitorings_schema) + + +def downgrade(): + op.add_column( + "t_sites_groups", + sa.Column( + "id_module", + sa.Integer(), + sa.ForeignKey( + f"gn_commons.t_modules.id_module", + name="fk_t_sites_groups_id_module", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=True, + ), + schema=monitorings_schema, + ) + + # LIMITATION: On ne prend que le premier module associé + statement = sa.text( + f""" + WITH sgm AS ( + SELECT id_sites_group , min(id_module) + FROM gn_monitoring.cor_sites_group_module + GROUP BY id_sites_group + ) + UPDATE gn_monitoring.t_sites_groups AS tsg + SET id_module = sgm.id_module + FROM sgm + WHERE tsg.id_sites_group = sgm.id_sites_group; + """ + ) + op.execute(statement) + + op.alter_column("t_sites_groups", "id_module", nullable=False, schema=monitorings_schema) + op.execute( + """ + DROP TABLE gn_monitoring.cor_sites_group_module; + """ + ) diff --git a/backend/gn_module_monitoring/migrations/f3413cccdfa8_add_geom_column_to_sites_group.py b/backend/gn_module_monitoring/migrations/f3413cccdfa8_add_geom_column_to_sites_group.py new file mode 100644 index 000000000..cb03a88bc --- /dev/null +++ b/backend/gn_module_monitoring/migrations/f3413cccdfa8_add_geom_column_to_sites_group.py @@ -0,0 +1,120 @@ +"""add geom column to sites_group + +Revision ID: f3413cccdfa8 +Revises: f24adb481f54 +Create Date: 2023-09-26 10:57:18.886119 + +""" + +from alembic import op +import sqlalchemy as sa +import geoalchemy2 + +# revision identifiers, used by Alembic. +revision = "f3413cccdfa8" +down_revision = "f24adb481f54" +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute( + """ + ALTER TABLE gn_monitoring.t_sites_groups + ADD COLUMN geom + public.geometry(geometry, 4326) NULL, + ADD COLUMN geom_local + public.geometry(geometry, 2154) NULL, + ADD COLUMN altitude_min + int4 NULL, + ADD COLUMN altitude_max + int4 NULL; + """ + ) + + # version sqlalchemy + # op.add_column( + # schema="gn_monitoring", + # table_name="t_sites_groups", + # column=sa.Column( + # "geom", + # geoalchemy2.types.Geometry(geometry_type='GEOMETRY'), + # nullable=True, + # ) + # ) + + # op.add_column( + # schema="gn_monitoring", + # table_name="t_sites_groups", + # column=sa.Column( + # "geom_local", + # geoalchemy2.types.Geometry(geometry_type='GEOMETRY'), + # nullable=True, + # ) + # ) + + op.execute( + """ + ALTER TABLE gn_monitoring.t_sites_groups + ADD CONSTRAINT enforce_srid_geom CHECK ((st_srid(geom) = 4326)); + """ + ) + + op.execute( + """ + CREATE INDEX idx_t_sites_groups_geom ON gn_monitoring.t_sites_groups USING gist (geom); + """ + ) + + op.execute( + """ + create trigger tri_calculate_geom_local before + insert + or + update + on + gn_monitoring.t_sites_groups for each row execute function ref_geo.fct_trg_calculate_geom_local('geom', + 'geom_local'); + create trigger tri_t_sites_groups_calculate_alt before + insert + or + update + on + gn_monitoring.t_sites_groups for each row execute function ref_geo.fct_trg_calculate_alt_minmax('geom'); + create trigger tri_insert_calculate_altitude before + insert + on + gn_monitoring.t_sites_groups for each row execute function ref_geo.fct_trg_calculate_alt_minmax('geom'); + create trigger tri_update_calculate_altitude before + update + of geom_local, + geom on + gn_monitoring.t_sites_groups for each row execute function ref_geo.fct_trg_calculate_alt_minmax('geom'); + + """ + ) + + +def downgrade(): + op.execute( + """ + DROP TRIGGER tri_calculate_geom_local + ON gn_monitoring.t_sites_groups; + DROP TRIGGER tri_t_sites_groups_calculate_alt + ON gn_monitoring.t_sites_groups; + DROP TRIGGER tri_insert_calculate_altitude + ON gn_monitoring.t_sites_groups; + DROP TRIGGER tri_update_calculate_altitude + ON gn_monitoring.t_sites_groups; + """ + ) + + op.execute( + """ + ALTER TABLE gn_monitoring.t_sites_groups + DROP COLUMN geom, + DROP COLUMN geom_local, + DROP COLUMN altitude_min, + DROP COLUMN altitude_max; + """ + ) diff --git a/backend/gn_module_monitoring/migrations/fc90d31c677f_declare_available_permissions.py b/backend/gn_module_monitoring/migrations/fc90d31c677f_declare_available_permissions.py index eba68daea..3e9378fc6 100644 --- a/backend/gn_module_monitoring/migrations/fc90d31c677f_declare_available_permissions.py +++ b/backend/gn_module_monitoring/migrations/fc90d31c677f_declare_available_permissions.py @@ -5,6 +5,7 @@ Create Date: 2023-06-09 10:32:21.008918 """ + from alembic import op import sqlalchemy as sa diff --git a/backend/gn_module_monitoring/modules/repositories.py b/backend/gn_module_monitoring/modules/repositories.py index e93052fb5..83d149812 100644 --- a/backend/gn_module_monitoring/modules/repositories.py +++ b/backend/gn_module_monitoring/modules/repositories.py @@ -4,14 +4,15 @@ get_modules """ +from sqlalchemy import select from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound from geonature.utils.env import DB from geonature.utils.errors import GeoNatureError - from geonature.core.gn_commons.models import TModules from geonature.core.gn_synthese.models import TSources -from ..monitoring.models import TMonitoringModules + +from gn_module_monitoring.monitoring.models import TMonitoringModules def get_simple_module(field_name, value): @@ -47,17 +48,23 @@ def get_module(field_name, value, moduleCls=TMonitoringModules): :rtype : dict """ - if not hasattr(moduleCls, field_name): raise GeoNatureError( "get_module : TMonitoringModules ne possède pas de champs {}".format(field_name) ) + if value == "generic": + return None + try: - module = DB.session.query(moduleCls).filter(getattr(moduleCls, field_name) == value).one() + module = DB.session.execute( + select(moduleCls).where(getattr(moduleCls, field_name) == value) + ).scalar_one() return module + except NoResultFound as e: + raise e except MultipleResultsFound: raise GeoNatureError( "get_module : multiple results found for field_name {} and value {}".format( @@ -81,7 +88,9 @@ def get_modules(session=None): if not session: session = DB.session try: - res = session.query(TMonitoringModules).order_by(TMonitoringModules.module_label).all() + res = session.scalars( + select(TMonitoringModules).order_by(TMonitoringModules.module_label) + ).all() return res @@ -92,7 +101,9 @@ def get_modules(session=None): def get_source_by_code(value): try: - source = DB.session.query(TSources).filter(TSources.name_source == value).one() + source = DB.session.execute( + select(TSources).where(TSources.name_source == value) + ).scalar_one() return source diff --git a/backend/gn_module_monitoring/monitoring/admin.py b/backend/gn_module_monitoring/monitoring/admin.py new file mode 100644 index 000000000..7c4485510 --- /dev/null +++ b/backend/gn_module_monitoring/monitoring/admin.py @@ -0,0 +1,95 @@ +from flask import json + +from flask_admin.contrib.sqla import ModelView +from flask_admin.form import fields + +from sqlalchemy import exists +from wtforms.validators import ValidationError + +from geonature.utils.env import DB +from geonature.core.admin.admin import CruvedProtectedMixin + +from pypnnomenclature.models import BibNomenclaturesTypes, TNomenclatures + +from geonature.core.gn_monitoring.models import BibTypeSite +from gn_module_monitoring.monitoring.utils import json_formatter + +SITE_TYPE = "TYPE_SITE" + + +class Unique: + """validator that checks field uniqueness""" + + def __init__(self, model, field, compare_field, message=None): + self.model = model + self.field = field + self.compare_field = compare_field + if not message: + message = "A type is already created with this nomenclature" + self.message = message + + def __call__(self, form, field): + if field.object_data == field.data: + return + if DB.session.scalar( + exists() + .where(getattr(self.model, self.field) == getattr(field.data, self.compare_field)) + .select() + ): + raise ValidationError(self.message) + + +class JSONField(fields.JSONField): + def _value(self): + if self.raw_data: + return self.raw_data[0] + elif self.data: + return json.dumps(self.data, ensure_ascii=False, indent=2) + else: + return "" + + +class BibTypeSiteView(CruvedProtectedMixin, ModelView): + """ + Surcharge de l'administration des types de sites + """ + + module_code = "MONITORINGS" + object_code = "TYPES_SITES" + + def __init__(self, session, **kwargs): + # Référence au model utilisé + super(BibTypeSiteView, self).__init__(BibTypeSite, session, **kwargs) + + def get_only_nomenclature_asc(): + return ( + DB.session.query(TNomenclatures) + .join(TNomenclatures.nomenclature_type) + .filter(BibNomenclaturesTypes.mnemonique == SITE_TYPE) + .order_by(TNomenclatures.label_fr.asc()) + ) + + def get_label_fr_nomenclature(x): + return x.label_fr + + def list_label_nomenclature_formatter(view, _context, model, _name): + return model.nomenclature.label_fr + + # Nom de colonne user friendly + column_labels = dict(nomenclature="Types de site") + # Description des colonnes + column_descriptions = dict(nomenclature="Nomenclature de type de site à choisir") + + column_hide_backrefs = False + + form_args = dict( + nomenclature=dict( + query_factory=get_only_nomenclature_asc, + get_label=get_label_fr_nomenclature, + validators=[Unique(BibTypeSite, "id_nomenclature_type_site", "id_nomenclature")], + ) + ) + form_overrides = {"config": JSONField} + column_list = ("nomenclature", "config") + column_formatters = dict(nomenclature=list_label_nomenclature_formatter, config=json_formatter) + form_excluded_columns = "sites" diff --git a/backend/gn_module_monitoring/monitoring/base.py b/backend/gn_module_monitoring/monitoring/base.py index ab331e005..37233953f 100644 --- a/backend/gn_module_monitoring/monitoring/base.py +++ b/backend/gn_module_monitoring/monitoring/base.py @@ -1,11 +1,5 @@ from geonature.utils.errors import GeoNatureError -from ..config.repositories import ( - config_param as repositories_config_param, - config_schema as repositories_config_schema, - get_config as repositories_get_config, -) - class MonitoringDefinitions: """ @@ -39,8 +33,18 @@ def MonitoringObject(self, object_type): ) ) - def monitoring_object_instance(self, module_code, object_type, id=None, model=None): - return self.MonitoringObject(object_type)(module_code, object_type, id, model) + def monitoring_object_instance( + self, + module_code, + object_type, + config, + id=None, + model=None, + ): + # force config + return self.MonitoringObject(object_type)( + module_code, object_type, config=config, id=id, model=model + ) def MonitoringModel(self, object_type): try: @@ -60,16 +64,23 @@ class MonitoringObjectBase: _object_type = None _module_code = None _id = None + _config = None _model = None _children = {} _parent = None + cruved = {} + + def __init__(self, module_code, object_type, config, id=None, model=None): + if module_code == "generic": + module_code = "MONITORINGS" - def __init__(self, module_code, object_type, id=None, model=None): self._module_code = module_code + self._object_type = object_type self._id = id + self._config = config if not self._id and model: self._id = getattr(model, self.config_param("id_field_name")) @@ -89,7 +100,6 @@ def MonitoringModel(self): try: Model = monitoring_definitions.MonitoringModel(self._object_type) return Model - pass except Exception: pass @@ -100,11 +110,27 @@ def MonitoringModel(self): Model = monitoring_definitions.MonitoringModel(new_object_type) return Model - def config(self): - return repositories_get_config(self._module_code) + def config(self, force=False): + return self._config def config_param(self, param_name): - return repositories_config_param(self._module_code, self._object_type, param_name) + """ + revoie un parametre de la configuration des objets + + :param param_name: le parametre voulu (id_field_name, label) + :return: valeur du paramètre requis + :rtype: str + + :Exemple: + + config_param('id_field_name') + renverra 'id_base_site' + + config_param('label') + renverra 'Site' + + """ + return self._config[self._object_type].get(param_name) def get_value_generic(self, param_name): if not hasattr(self._model, param_name): @@ -133,11 +159,38 @@ def parent_type(self): def parent_config_param(self, param_name): parent_type = self.parent_type() if parent_type: - return repositories_config_param(self._module_code, parent_type, param_name) + return self._config[parent_type].get(param_name) def config_schema(self, type_schema="all"): - return repositories_config_schema(self._module_code, self._object_type, type_schema) - pass + """ + renvoie une liste d'éléments de configuration de formulaire + + pour type_schema: + 'generic' : renvoie le schema générique + 'specific' : renvoie le schema spécifique + 'all': par defaut renvoie tout le schema + + Un élément est un dictionaire de type + { + "attribut_name": "id_base_site", + "Label": "Id du site", + "type_widget": "integer", + "required": "true", + } + + :param module_code: reference le module concerne + :param object_type: le type d'object (site, visit, obervation) + :param type_schema: le type de schema requis ('all', 'generic', 'specific') + :return: tableau d'élément de configuration de formulaire + :rtype: list + """ + if type_schema in ["generic", "specific"]: + return self._config[self._object_type][type_schema] + + # renvoie le schema complet si type_schema == 'all' ou par defaut + schema = dict(self._config[self._object_type]["generic"]) + schema.update(self._config[self._object_type]["specific"]) + return schema # def base_type_object(self): # """ diff --git a/backend/gn_module_monitoring/monitoring/definitions.py b/backend/gn_module_monitoring/monitoring/definitions.py index fd6b0c49f..d4763329f 100644 --- a/backend/gn_module_monitoring/monitoring/definitions.py +++ b/backend/gn_module_monitoring/monitoring/definitions.py @@ -1,4 +1,4 @@ -from .models import ( +from gn_module_monitoring.monitoring.models import ( TMonitoringModules, TMonitoringSites, TMonitoringVisits, @@ -6,11 +6,10 @@ TMonitoringObservationDetails, TMonitoringSitesGroups, ) -from .objects import MonitoringModule, MonitoringSite - -from .base import monitoring_definitions -from .repositories import MonitoringObject -from .geom import MonitoringObjectGeom +from gn_module_monitoring.monitoring.objects import MonitoringModule, MonitoringSite +from gn_module_monitoring.monitoring.base import monitoring_definitions +from gn_module_monitoring.monitoring.repositories import MonitoringObject +from gn_module_monitoring.monitoring.geom import MonitoringObjectGeom """ diff --git a/backend/gn_module_monitoring/monitoring/geom.py b/backend/gn_module_monitoring/monitoring/geom.py index b520c1226..29dfe8e6f 100644 --- a/backend/gn_module_monitoring/monitoring/geom.py +++ b/backend/gn_module_monitoring/monitoring/geom.py @@ -1,6 +1,9 @@ import json -from .repositories import MonitoringObject +from marshmallow import EXCLUDE + +from gn_module_monitoring.monitoring.repositories import MonitoringObject +from gn_module_monitoring.monitoring.serializer import MonitoringSerializer_dict class MonitoringObjectGeom(MonitoringObject): @@ -12,24 +15,65 @@ def as_geofeature(self, depth=None, columns=()): geom_field_name, id_field_name, depth=depth, columns=columns ) - def serialize(self, depth): - monitoring_object_dict = MonitoringObject.serialize(self, depth) - geometry = {} + def create_or_update(self, post_data): + # security : remove modules if no id_module provided + checked_module = [] + if "modules" in post_data["properties"]: + if type(post_data["properties"]["modules"]) is list: + for module in post_data["properties"]["modules"]: + if type(module) is dict: + if "id_module" in module: + checked_module.append(module) + if type(module) is int: + checked_module.append(module) + post_data["properties"]["modules"] = checked_module - if hasattr(self._model, "geom_geojson"): - geom_geojson = getattr(self._model, "geom_geojson") + return super().create_or_update(post_data) - geometry = ( - json.loads( - # self._model.__dict__.get('geom_geojson') - getattr(self._model, "geom_geojson") - ) - if geom_geojson - else None + def serialize(self, depth, is_child=False): + monitoring_object_dict = super(MonitoringObject, self).serialize(depth, is_child) + + if len(monitoring_object_dict["properties"].get("types_site", [])) != 0: + if hasattr(self._model, "types_site"): + # TODO: performance? + types_site = [typ.nomenclature.label_fr for typ in self._model.types_site] + # On récupères tous les ids des types de site associé au site (nécessaire pour garder l'ensemble des types de site associé à un site) + ids_types_site = [typ.id_nomenclature_type_site for typ in self._model.types_site] + monitoring_object_dict["properties"]["types_site"] = types_site + monitoring_object_dict["properties"]["ids_types_site"] = ids_types_site + + # On ne sérialise la géométrie que si l'objet n'est pas un enfant + # si l'objet est de type enfant il va être affiché au niveau du tableau + # et sa géométrie sera récupérée a partir de la route /geometrie + if not is_child: + geometry = {} + dump_object = MonitoringSerializer_dict[self._object_type](unknown=EXCLUDE).dump( + self._model ) - if not self._id: - geometry = None + # monitoring_object_dict['properties'] = dump_object + if "geometry" in dump_object and self._model.geom is not None: + geometry = ( + json.loads( + # self._model.__dict__.get('geom_geojson') + dump_object["geometry"] + ) + if dump_object["geometry"] + else None + ) + + elif hasattr(self._model, "geom_geojson"): + geom_geojson = getattr(self._model, "geom_geojson") - monitoring_object_dict["geometry"] = geometry + geometry = ( + json.loads( + # self._model.__dict__.get('geom_geojson') + getattr(self._model, "geom_geojson") + ) + if geom_geojson + else None + ) + if not self._id: + geometry = None + monitoring_object_dict["geometry"] = geometry return monitoring_object_dict diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index 916799a56..596aa6be9 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -1,24 +1,81 @@ """ Modèles SQLAlchemy pour les modules de suivi """ -from sqlalchemy import select, func, and_ -from sqlalchemy.orm import column_property -from sqlalchemy.dialects.postgresql import JSONB, UUID + +import geoalchemy2 + +from flask import g + from uuid import uuid4 +from sqlalchemy import join, select, func, and_ +from sqlalchemy.orm import ( + column_property, + aliased, +) +from sqlalchemy.dialects.postgresql import JSONB, UUID + from utils_flask_sqla.serializers import serializable from utils_flask_sqla_geo.serializers import geoserializable from sqlalchemy.ext.hybrid import hybrid_property - +from geonature.utils.env import DB from geonature.core.gn_commons.models import TMedias -from geonature.core.gn_monitoring.models import TBaseSites, TBaseVisits +from geonature.core.gn_monitoring.models import ( + TBaseSites, + TBaseVisits, + cor_module_type, + cor_site_type, + BibTypeSite, + cor_visit_observer, + TObservations, +) from geonature.core.gn_meta.models import TDatasets -from geonature.utils.env import DB from geonature.core.gn_commons.models import TModules, cor_module_dataset +from geonature.core.gn_permissions.tools import has_any_permissions_by_action + from pypnusershub.db.models import User -from geonature.core.gn_monitoring.models import corVisitObserver + +from gn_module_monitoring.monitoring.queries import ( + GnMonitoringGenericFilter as MonitoringQuery, + SitesQuery, + SitesGroupsQuery, + VisitQuery, + ObservationsQuery, +) + + +class PermissionModel: + def has_permission( + self, + cruved_object={"C": False, "R": False, "U": False, "D": False, "E": False, "V": False}, + ): + cruved_object_out = {} + for action_key, action_value in cruved_object.items(): + cruved_object_out[action_key] = self.has_instance_permission(scope=action_value) + return cruved_object_out + + def get_permission_by_action(self, module_code=None, object_code=None): + return has_any_permissions_by_action(module_code=module_code, object_code=object_code) + + +cor_sites_group_module = DB.Table( + "cor_sites_group_module", + DB.Column( + "id_sites_group", + DB.Integer, + DB.ForeignKey("gn_monitoring.t_sites_groups.id_sites_group"), + primary_key=True, + ), + DB.Column( + "id_module", + DB.Integer, + DB.ForeignKey(TModules.id_module), + primary_key=True, + ), + schema="gn_monitoring", +) @serializable @@ -34,40 +91,14 @@ class TMonitoringObservationDetails(DB.Model): medias = DB.relationship( TMedias, - lazy="joined", primaryjoin=(TMedias.uuid_attached_row == uuid_observation_detail), foreign_keys=[TMedias.uuid_attached_row], + overlaps="medias,medias", ) @serializable -class TObservations(DB.Model): - __tablename__ = "t_observations" - __table_args__ = {"schema": "gn_monitoring"} - - id_observation = DB.Column(DB.Integer, primary_key=True, nullable=False, unique=True) - id_base_visit = DB.Column(DB.ForeignKey("gn_monitoring.t_base_visits.id_base_visit")) - cd_nom = DB.Column(DB.Integer) - comments = DB.Column(DB.String) - uuid_observation = DB.Column(UUID(as_uuid=True), default=uuid4) - - medias = DB.relationship( - TMedias, - lazy="joined", - primaryjoin=(TMedias.uuid_attached_row == uuid_observation), - foreign_keys=[TMedias.uuid_attached_row], - ) - - observation_details = DB.relation( - TMonitoringObservationDetails, - primaryjoin=(id_observation == TMonitoringObservationDetails.id_observation), - foreign_keys=[TMonitoringObservationDetails.id_observation], - cascade="all,delete", - ) - - -@serializable -class TMonitoringObservations(TObservations): +class TMonitoringObservations(TObservations, PermissionModel, ObservationsQuery): __tablename__ = "t_observation_complements" __table_args__ = {"schema": "gn_monitoring"} __mapper_args__ = { @@ -82,12 +113,51 @@ class TMonitoringObservations(TObservations): nullable=False, ) + medias = DB.relationship( + TMedias, + primaryjoin=(TMedias.uuid_attached_row == TObservations.uuid_observation), + foreign_keys=[TMedias.uuid_attached_row], + overlaps="medias", + ) + + observation_details = DB.relation( + TMonitoringObservationDetails, + primaryjoin=(id_observation == TMonitoringObservationDetails.id_observation), + foreign_keys=[TMonitoringObservationDetails.id_observation], + cascade="all,delete", + ) -TBaseVisits.dataset = DB.relationship(TDatasets) + @hybrid_property + def organism_actors(self): + # return self.digitiser.id_organisme + actors_organism_list = [] + if isinstance(self.digitiser, list): + for actor in self.digitiser: + if actor.id_organisme is not None: + actors_organism_list.append(actor.id_organisme) + elif isinstance(self.digitiser, User): + actors_organism_list.append(self.digitiser.id_organisme) + return actors_organism_list + + def has_instance_permission(self, scope): + if scope == 0: + return False + elif scope in (1, 2): + if ( + g.current_user.id_role == self.id_digitiser + ): # or g.current_user in self.user_actors: + return True + if scope == 2 and g.current_user.id_organisme in self.organism_actors: + return True + elif scope == 3: + return True + + +# TBaseVisits.dataset = DB.relationship(TDatasets) @serializable -class TMonitoringVisits(TBaseVisits): +class TMonitoringVisits(TBaseVisits, PermissionModel, VisitQuery): __tablename__ = "t_visit_complements" __table_args__ = {"schema": "gn_monitoring"} __mapper_args__ = { @@ -104,12 +174,12 @@ class TMonitoringVisits(TBaseVisits): medias = DB.relationship( TMedias, - lazy="joined", primaryjoin=(TMedias.uuid_attached_row == TBaseVisits.uuid_base_visit), foreign_keys=[TMedias.uuid_attached_row], + overlaps="medias,medias", ) - observers = DB.relationship(User, lazy="joined", secondary=corVisitObserver) + observers = DB.relationship(User, lazy="joined", secondary=cor_visit_observer) observations = DB.relation( "TMonitoringObservations", @@ -120,13 +190,51 @@ class TMonitoringVisits(TBaseVisits): ) nb_observations = column_property( - select([func.count(TObservations.id_base_visit)]).where( - TObservations.id_base_visit == id_base_visit - ) + select(func.count(TObservations.id_base_visit)) + .where(TObservations.id_base_visit == id_base_visit) + .scalar_subquery() ) -@geoserializable -class TMonitoringSites(TBaseSites): + module = DB.relationship( + TModules, + lazy="select", + primaryjoin=(TModules.id_module == TBaseVisits.id_module), + foreign_keys=[TBaseVisits.id_module], + uselist=False, + ) + + @hybrid_property + def organism_actors(self): + # return self.digitiser.id_organisme + actors_organism_list = [] + if isinstance(self.digitiser, list): + for actor in self.digitiser: + if actor.id_organisme is not None: + actors_organism_list.append(actor.id_organisme) + elif isinstance(self.observers, list): + for actor in self.observers: + if actor.id_organisme is not None: + actors_organism_list.append(actor.id_organisme) + elif isinstance(self.digitiser, User): + actors_organism_list.append(self.digitiser.id_organisme) + return actors_organism_list + + def has_instance_permission(self, scope): + if scope == 0: + return False + elif scope in (1, 2): + if g.current_user.id_role == self.id_digitiser or any( + observer.id_role == g.current_user.id_role for observer in self.observers + ): # or g.current_user in self.user_actors: + return True + if scope == 2 and g.current_user.id_organisme in self.organism_actors: + return True + elif scope == 3: + return True + + +@geoserializable(geoCol="geom", idCol="id_base_site") +class TMonitoringSites(TBaseSites, PermissionModel, SitesQuery): __tablename__ = "t_site_complements" __table_args__ = {"schema": "gn_monitoring"} __mapper_args__ = { @@ -137,11 +245,6 @@ class TMonitoringSites(TBaseSites): DB.ForeignKey("gn_monitoring.t_base_sites.id_base_site"), nullable=False, primary_key=True ) - id_module = DB.Column( - DB.ForeignKey("gn_commons.t_modules.id_module"), - nullable=False, - ) - id_sites_group = DB.Column( DB.ForeignKey( "gn_monitoring.t_sites_groups.id_sites_group", @@ -158,42 +261,72 @@ class TMonitoringSites(TBaseSites): primaryjoin=(TBaseSites.id_base_site == TBaseVisits.id_base_site), foreign_keys=[TBaseVisits.id_base_site], cascade="all,delete", + overlaps="t_base_visits", ) medias = DB.relationship( TMedias, - lazy="joined", + lazy="select", primaryjoin=(TMedias.uuid_attached_row == TBaseSites.uuid_base_site), foreign_keys=[TMedias.uuid_attached_row], cascade="all", + overlaps="medias", ) last_visit = column_property( - select([func.max(TBaseVisits.visit_date_min)]).where( - TBaseVisits.id_base_site == id_base_site - ) + select(func.max(TBaseVisits.visit_date_min)) + .where(TBaseVisits.id_base_site == id_base_site) + .scalar_subquery() ) nb_visits = column_property( - select([func.count(TBaseVisits.id_base_site)]).where( - TBaseVisits.id_base_site == id_base_site - ) + select(func.count(TBaseVisits.id_base_site)) + .where(TBaseVisits.id_base_site == id_base_site) + .scalar_subquery() ) geom_geojson = column_property(func.ST_AsGeoJSON(TBaseSites.geom), deferred=True) - - -@serializable -class TMonitoringSitesGroups(DB.Model): + types_site = DB.relationship("BibTypeSite", secondary=cor_site_type, overlaps="sites") + + @hybrid_property + def organism_actors(self): + actors_organism_list = [] + if isinstance(self.inventor, list): + for actor in self.inventor: + if actor.id_organisme is not None: + actors_organism_list.append(actor.id_organisme) + else: + if hasattr(self.inventor, "id_organisme"): + actors_organism_list.append(self.inventor.id_organisme) + return actors_organism_list + + def has_instance_permission(self, scope): + if scope == 0: + return False + elif scope in (1, 2): + if ( + g.current_user.id_role == self.id_digitiser + or g.current_user.id_role == self.id_inventor + ): # or g.current_user in self.user_actors: + return True + if scope == 2 and g.current_user.id_organisme in self.organism_actors: + return True + elif scope == 3: + return True + return False + + +@geoserializable(geoCol="geom", idCol="id_sites_group") +class TMonitoringSitesGroups(DB.Model, PermissionModel, SitesGroupsQuery): __tablename__ = "t_sites_groups" __table_args__ = {"schema": "gn_monitoring"} id_sites_group = DB.Column(DB.Integer, primary_key=True, nullable=False, unique=True) + id_digitiser = DB.Column(DB.Integer, DB.ForeignKey("utilisateurs.t_roles.id_role")) - id_module = DB.Column( - DB.ForeignKey("gn_commons.t_modules.id_module"), nullable=False, unique=True + digitiser = DB.relationship( + User, primaryjoin=(User.id_role == id_digitiser), foreign_keys=[id_digitiser] ) - uuid_sites_group = DB.Column(UUID(as_uuid=True), default=uuid4) sites_group_name = DB.Column(DB.Unicode) @@ -201,14 +334,14 @@ class TMonitoringSitesGroups(DB.Model): sites_group_description = DB.Column(DB.Unicode) comments = DB.Column(DB.Unicode) - + geom = DB.Column(geoalchemy2.types.Geometry("GEOMETRY", 4326, nullable=True)) data = DB.Column(JSONB) medias = DB.relationship( TMedias, primaryjoin=(TMedias.uuid_attached_row == uuid_sites_group), foreign_keys=[TMedias.uuid_attached_row], - lazy="joined", + overlaps="medias", ) sites = DB.relationship( @@ -216,27 +349,68 @@ class TMonitoringSitesGroups(DB.Model): uselist=True, # pourquoi pas par defaut ? primaryjoin=(TMonitoringSites.id_sites_group == id_sites_group), foreign_keys=[TMonitoringSites.id_sites_group], - lazy="joined", + lazy="select", + ) + modules = DB.relationship( + "TMonitoringModules", + secondary=cor_sites_group_module, + uselist=True, + back_populates="sites_groups", ) nb_sites = column_property( - select([func.count(TMonitoringSites.id_sites_group)]).where( - TMonitoringSites.id_sites_group == id_sites_group - ) + select(func.count(TMonitoringSites.id_sites_group)) + .where(TMonitoringSites.id_sites_group == id_sites_group) + .scalar_subquery() ) + altitude_min = DB.Column(DB.Integer) + altitude_max = DB.Column(DB.Integer) nb_visits = column_property( - select([func.count(TMonitoringVisits.id_base_site)]).where( - and_( - TMonitoringVisits.id_base_site == TMonitoringSites.id_base_site, - TMonitoringSites.id_sites_group == id_sites_group, - ) + select(func.count(TMonitoringVisits.id_base_site)) + .where( + TMonitoringVisits.id_base_site == TMonitoringSites.id_base_site, + TMonitoringSites.id_sites_group == id_sites_group, ) + .scalar_subquery() ) + geom_geojson = column_property( + select(func.st_asgeojson(func.st_convexHull(func.st_collect(TMonitoringSites.geom)))) + .where( + TMonitoringSites.id_sites_group == id_sites_group, + ) + .scalar_subquery() + ) + + @hybrid_property + def organism_actors(self): + # return self.digitiser.id_organisme + actors_organism_list = [] + if isinstance(self.digitiser, list): + for actor in self.digitiser: + if actor.id_organisme is not None: + actors_organism_list.append(actor.id_organisme) + elif isinstance(self.digitiser, User): + actors_organism_list.append(self.digitiser.id_organisme) + return actors_organism_list + + def has_instance_permission(self, scope): + if scope == 0: + return False + elif scope in (1, 2): + if ( + g.current_user.id_role == self.id_digitiser + ): # or g.current_user in self.user_actors: + return True + if scope == 2 and g.current_user.id_organisme in self.organism_actors: + return True + elif scope == 3: + return True + @serializable -class TMonitoringModules(TModules): +class TMonitoringModules(TModules, PermissionModel, MonitoringQuery): __tablename__ = "t_module_complements" __table_args__ = {"schema": "gn_monitoring"} __mapper_args__ = { @@ -263,30 +437,38 @@ class TMonitoringModules(TModules): TMedias, primaryjoin=(TMedias.uuid_attached_row == uuid_module_complement), foreign_keys=[TMedias.uuid_attached_row], - lazy="joined", + lazy="select", + overlaps="medias,medias", ) + # TODO: restore it with CorCategorySite sites = DB.relationship( "TMonitoringSites", uselist=True, # pourquoi pas par defaut ? - primaryjoin=TMonitoringSites.id_module == id_module, - foreign_keys=[id_module], + primaryjoin=(id_module == cor_module_type.c.id_module), + secondaryjoin=(TMonitoringSites.id_base_site == cor_site_type.c.id_base_site), + secondary=join( + cor_site_type, + cor_module_type, + cor_site_type.c.id_type_site == cor_module_type.c.id_type_site, + ), + foreign_keys=[cor_site_type.c.id_base_site, cor_module_type.c.id_module], lazy="select", + viewonly=True, ) - sites_groups = DB.relationship( - "TMonitoringSitesGroups", - uselist=True, # pourquoi pas par defaut ? - primaryjoin=TMonitoringSitesGroups.id_module == id_module, - foreign_keys=[id_module], - lazy="select", - ) + sites_groups = DB.relationship(TMonitoringSitesGroups, secondary=cor_sites_group_module) datasets = DB.relationship( "TDatasets", secondary=cor_module_dataset, join_depth=0, - lazy="joined", + overlaps="modules", + ) + + types_site = DB.relationship( + "BibTypeSite", + secondary=cor_module_type, ) data = DB.Column(JSONB) @@ -298,54 +480,11 @@ class TMonitoringModules(TModules): # foreign_keys=[TBaseVisits.id_module], # cascade="all,delete" # ) - - -TMonitoringModules.visits = DB.relationship( - TMonitoringVisits, - lazy="select", - primaryjoin=(TMonitoringModules.id_module == TMonitoringVisits.id_module), - foreign_keys=[TMonitoringVisits.id_module], - cascade="all", -) - - -# add sites_group relationship to TMonitoringSites - -TMonitoringSites.sites_group = DB.relationship( - TMonitoringSitesGroups, - primaryjoin=(TMonitoringSitesGroups.id_sites_group == TMonitoringSites.id_sites_group), - cascade="all", - lazy="select", - uselist=False, -) - -TMonitoringSitesGroups.visits = DB.relationship( - TMonitoringVisits, - primaryjoin=(TMonitoringSites.id_sites_group == TMonitoringSitesGroups.id_sites_group), - secondaryjoin=(TMonitoringVisits.id_base_site == TMonitoringSites.id_base_site), - secondary="gn_monitoring.t_site_complements", -) - -TMonitoringSitesGroups.nb_visits = column_property( - select([func.count(TMonitoringVisits.id_base_site)]).where( - and_( - TMonitoringVisits.id_base_site == TMonitoringSites.id_base_site, - TMonitoringSites.id_sites_group == TMonitoringSitesGroups.id_sites_group, - ) - ) -) - -# note the alias is mandotory otherwise the where is done on the subquery table -# and not the global TMonitoring table -TMonitoringSitesGroups.geom_geojson = column_property( - select([func.st_asgeojson(func.st_convexHull(func.st_collect(TBaseSites.geom)))]) - .select_from( - TMonitoringSitesGroups.__table__.alias("subquery").join( - TMonitoringSites, - TMonitoringSites.id_sites_group == TMonitoringSitesGroups.id_sites_group, - ) - ) - .where( - TMonitoringSites.id_sites_group == TMonitoringSitesGroups.id_sites_group, + visits = DB.relationship( + TMonitoringVisits, + lazy="select", + primaryjoin=(id_module == TMonitoringVisits.id_module), + foreign_keys=[TMonitoringVisits.id_module], + cascade="all", + overlaps="sites,sites_group,module", ) -) diff --git a/backend/gn_module_monitoring/monitoring/objects.py b/backend/gn_module_monitoring/monitoring/objects.py index edd51870e..78b9eaae0 100644 --- a/backend/gn_module_monitoring/monitoring/objects.py +++ b/backend/gn_module_monitoring/monitoring/objects.py @@ -1,7 +1,7 @@ -from .repositories import MonitoringObject -from .geom import MonitoringObjectGeom from geonature.utils.env import DB -from geonature.core.gn_commons.models import TModules + +from gn_module_monitoring.monitoring.repositories import MonitoringObject +from gn_module_monitoring.monitoring.geom import MonitoringObjectGeom class MonitoringModule(MonitoringObject): @@ -25,10 +25,31 @@ class MonitoringSite(MonitoringObjectGeom): avec la méthode from_dict """ - def preprocess_data(self, data): - module_ids = [module.id_module for module in self._model.modules] - id_module = int(data["id_module"]) - if id_module not in module_ids: - module_ids.append(id_module) + def preprocess_data(self, properties, data=[]): + if all(isinstance(x, int) for x in properties["types_site"]): + return + # TODO: VERIFIER CE QUI EST NECESSAIRE A GARDER ICI + if len(data) != 0: + if len(data["types_site"]) > 0 and all(isinstance(x, int) for x in data["types_site"]): + properties["types_site"] = data["types_site"] + + elif len(properties.get("types_site", [])) != 0: + if hasattr(self._model, "types_site"): + properties["types_site"] = data["types_site"] + + elif "data" in data and data["data"]["id_nomenclature_type_site"]: + properties["id_nomenclature_type_site"] = data["data"]["id_nomenclature_type_site"] + else: + properties["id_nomenclature_type_site"] = data["types_site"][0][ + "id_nomenclature_type_site" + ] - data["modules"] = module_ids + # properties["types_site"] = [] + # # TODO: performance? + # # for type in properties['types_site']: + # # properties['types_site'].append(types_site) + # types_site = [ + # typ.nomenclature.id_nomenclature for typ in self._model.types_site + # ] + # properties["types_site"] = types_site + # TODO: A enlever une fois qu'on aura enelever le champ "id_nomenclature_type_site" du model et de la bdd diff --git a/backend/gn_module_monitoring/monitoring/queries.py b/backend/gn_module_monitoring/monitoring/queries.py new file mode 100644 index 000000000..d00b598e3 --- /dev/null +++ b/backend/gn_module_monitoring/monitoring/queries.py @@ -0,0 +1,324 @@ +from flask import g + +from copy import copy + +from sqlalchemy import Unicode, and_, Unicode, func, or_, false, true, select +from sqlalchemy.orm import class_mapper +from sqlalchemy.types import DateTime +from sqlalchemy.sql.expression import Select +from werkzeug.datastructures import MultiDict +from sqlalchemy.orm import aliased + +from pypnusershub.db.models import User +from apptax.taxonomie.models import Taxref + +from geonature.utils.env import db + +from geonature.core.gn_permissions.tools import get_scopes_by_action +from geonature.core.gn_commons.models import TModules +from pypnnomenclature.models import TNomenclatures +import gn_module_monitoring.monitoring.models as Models + + +class GnMonitoringGenericFilter: + @classmethod + def get_id_name(cls) -> None: + pk_string = class_mapper(cls).primary_key[0].name + if hasattr(cls, "id_g") == False: + pk_value = getattr(cls, pk_string) + setattr(cls, "id_g", pk_value) + return pk_string + + @classmethod + def filter_by_params(cls, query: Select, params: MultiDict = None, **kwargs): + and_list = [ + true(), + ] + params_copy = copy(params) + for key, value in params_copy.items(): + if hasattr(cls, key): + column = getattr(cls, key) + if not hasattr(column, "type"): + # is not an attribut + pass + elif isinstance(column.type, Unicode): + and_list.append(column.ilike(f"%{value}%")) + elif isinstance(column.type, DateTime): + and_list.append(func.to_char(column, "YYYY-MM-DD").ilike(f"%{value}%")) + elif key == "id_inventor" and not value.isdigit(): + join_inventor = aliased(User) + query = query.join(join_inventor, cls.inventor) + query = query.filter(join_inventor.nom_complet.ilike(f"%{value}%")) + else: + and_list.append(column == value) + params.pop(key) + + and_query = and_(*and_list) + return query.where(and_query) + + @classmethod + def sort(cls, query: Select, label: str, direction: str): + order_by = getattr(cls, label) + if direction == "desc": + order_by = order_by.desc() + + return query.order_by(order_by) + + @classmethod + def _get_cruved_scope(cls, module_code=None, object_code=None, user=None): + if user is None: + user = g.current_user + cruved = get_scopes_by_action( + id_role=user.id_role, module_code=module_code, object_code=object_code + ) + return cruved + + @classmethod + def _get_read_scope(cls, module_code="MONITORINGS", object_code=None, user=None): + if user is None: + user = g.current_user + cruved = get_scopes_by_action( + id_role=user.id_role, module_code=module_code, object_code=object_code + ) + return cruved["R"] + + @classmethod + def filter_by_readable( + cls, query: Select, module_code="MONITORINGS", object_code=None, user=None + ): + """ + Return the object where the user has autorization via its CRUVED + """ + return cls.filter_by_scope( + query=query, + scope=cls._get_read_scope(module_code=module_code, object_code=object_code, user=user), + ) + + +class SitesQuery(GnMonitoringGenericFilter): + @classmethod + def filter_by_scope(cls, query: Select, scope, user=None): + if user is None: + user = g.current_user + if scope == 0: + query = query.where(false()) + elif scope in (1, 2): + ors = [ + Models.TMonitoringSites.id_digitiser == user.id_role, + Models.TMonitoringSites.id_inventor == user.id_role, + ] + # if organism is None => do not filter on id_organism even if level = 2 + if scope == 2 and user.id_organisme is not None: + ors += [ + Models.TMonitoringSites.inventor.has(id_organisme=user.id_organisme), + Models.TMonitoringSites.digitiser.has(id_organisme=user.id_organisme), + ] + query = query.where(or_(*ors)) + return query + + @classmethod + def filter_by_params(cls, query: Select, params: MultiDict = None, **kwargs): + + if "modules" in params: + query = query.filter(cls.modules.any(id_module=params["modules"])) + params.pop("modules") + if "types_site_label" in params: + value = params["types_site_label"] + join_types_site = aliased(Models.BibTypeSite) + join_nomenclature_type_site = aliased(TNomenclatures) + query = query.join(join_types_site, cls.types_site) + query = query.join(join_nomenclature_type_site, join_types_site.nomenclature) + query = query.filter(join_nomenclature_type_site.label_default.ilike(f"%{value}%")) + if "types_site" in params: + value = params["types_site"] + if not isinstance(value, list): + value = [value] + query = query.filter( + cls.types_site.any(Models.BibTypeSite.id_nomenclature_type_site.in_(value)) + ) + + query = super().filter_by_params(query, params) + return query + + @classmethod + def filter_by_specific( + cls, + query: Select, + id_types_site: [] = None, + params: MultiDict = None, + **kwargs, + ): + if not id_types_site: + id_types_site = [] + + # Get specific + specific_config_models = ( + db.session.scalars( + select(Models.BibTypeSite).where( + Models.BibTypeSite.id_nomenclature_type_site.in_(id_types_site) + ) + ) + .unique() + .all() + ) + + specific_properties = {} + for s in specific_config_models: + if "specific" in (getattr(s, "config") or {}): + specific_properties.update(s.config["specific"]) + + for param, value in params.items(): + if param in specific_properties: + type = "text" + if "type_util" in specific_properties[param]: + type = specific_properties[param]["type_util"] + multiple = "false" + if "multiple" in specific_properties[param]: + multiple = specific_properties[param]["multiple"] + + if type in ("nomenclature", "taxonomy", "user", "area"): + join_table, join_column, filter_column = cls.get_relationship_clause(type) + if multiple == "true": + # Si la propriété est de type multiple + # Alors jointure sur chaque element de data->'params' + # extraction réalisée via fonction jsonb_array_elements_text avec une jointure lateral + # utilisation de correlate_except pour ne pas que t_base_site et t_sites_complement + # soient de nouveau dans la clause from + subquery_select = ( + select( + [ + func.jsonb_array_elements_text(cls.data[param]) + .cast(db.Integer) + .label("id") + ] + ) + .correlate_except() + .lateral() + ) + query = query.join(subquery_select, cls.data[param] != None) + query = query.join( + join_table, + subquery_select.c.id == join_column, + ) + else: + # Sinon type simple, jointure sur la valeur de data->'params' + query = query.join( + join_table, cls.data[param].astext.cast(db.Integer) == join_column + ) + # Filtre sur la valeur de la table de jointure + query = query.where(filter_column.ilike(f"{value}%")) + else: + # Sinon filtre texte simple + query = query.where(cls.data[param].astext.ilike(f"{value}%")) + + return query + + @classmethod + def get_relationship_clause( + cls, + type, + ): + join_table = None # alias de la table de jointure + join_column = None # nom de la colonne permettant la jointure entre data et la table + filter_column = None # nom de la colonne sur lequel le filtre est appliqué + if type == "nomenclature": + join_table = aliased(TNomenclatures) + join_column = join_table.id_nomenclature + filter_column = join_table.label_default + elif type == "taxonomy": + join_table = aliased(Taxref) + join_column = join_table.cd_nom + filter_column = join_table.nom_vern_or_lb_nom + elif type == "user": + join_table = aliased(User) + join_column = join_table.id_role + filter_column = join_table.nom_complet + elif type == "area": + pass + elif type == "habitat": + pass + + return join_table, join_column, filter_column + + +class SitesGroupsQuery(GnMonitoringGenericFilter): + @classmethod + def filter_by_scope(cls, query: Select, scope, user=None): + if user is None: + user = g.current_user + if scope == 0: + query = query.where(false()) + elif scope in (1, 2): + ors = [ + Models.TMonitoringSitesGroups.id_digitiser == user.id_role, + ] + # if organism is None => do not filter on id_organism even if level = 2 + if scope == 2 and user.id_organisme is not None: + ors += [ + Models.TMonitoringSitesGroups.digitiser.has(id_organisme=user.id_organisme) + ] + query = query.where(or_(*ors)) + return query + + @classmethod + def filter_by_params(cls, query: Select, params: MultiDict = None, **kwargs): + if "modules" in params: + value = params["modules"] + # Cas ou le filtre provient du gestionnaire des groupes de sites. + # La valeur est passée en chaine de caractère + if not value.isdigit(): + query = query.filter( + cls.modules.any(Models.TMonitoringModules.module_label.ilike(f"%{value}%")) + ) + else: + if not isinstance(value, list): + value = [value] + query = query.filter( + cls.modules.any(Models.TMonitoringModules.id_module.in_(value)) + ) + query = super().filter_by_params(query, params) + + return query + + +class VisitQuery(GnMonitoringGenericFilter): + @classmethod + def filter_by_scope(cls, query: Select, scope, user=None): + # Problem pas le même comportement que pour les sites et groupes de site + if user is None: + user = g.current_user + if scope == 0: + query = query.where(false()) + elif scope in (1, 2): + ors = [ + Models.TMonitoringVisits.id_digitiser == user.id_role, + Models.TMonitoringVisits.observers.any(id_role=user.id_role), + ] + # if organism is None => do not filter on id_organism even if level = 2 + if scope == 2 and user.id_organisme is not None: + ors += [ + Models.TMonitoringVisits.observers.any(id_organisme=user.id_organisme), + Models.TMonitoringVisits.digitiser.has(id_organisme=user.id_organisme), + ] + query = query.where(or_(*ors)) + return query + + +class ObservationsQuery(GnMonitoringGenericFilter): + @classmethod + def filter_by_scope(cls, query: Select, scope, user=None): + if user is None: + user = g.current_user + if scope == 0: + query = query.where(false()) + elif scope in (1, 2): + ors = [ + Models.TMonitoringObservations.id_digitiser == user.id_role, + ] + # if organism is None => do not filter on id_organism even if level = 2 + if scope == 2 and user.id_organisme is not None: + ors += [ + Models.TMonitoringObservations.digitiser.has(id_organisme=user.id_organisme) + ] + query = query.where(or_(*ors)) + return query diff --git a/backend/gn_module_monitoring/monitoring/repositories.py b/backend/gn_module_monitoring/monitoring/repositories.py index fa536efa2..a7f79eff1 100644 --- a/backend/gn_module_monitoring/monitoring/repositories.py +++ b/backend/gn_module_monitoring/monitoring/repositories.py @@ -1,12 +1,17 @@ +from flask import current_app + +from sqlalchemy import select + from geonature.utils.env import DB from geonature.utils.errors import GeoNatureError from geonature.core.gn_synthese.utils.process import import_from_table -from .serializer import MonitoringObjectSerializer -from ..config.repositories import get_config -import logging -from ..utils.utils import to_int -from sqlalchemy.orm import joinedload +from gn_module_monitoring.monitoring.serializer import MonitoringObjectSerializer +from gn_module_monitoring.utils.utils import to_int +from gn_module_monitoring.utils.routes import get_objet_with_permission_boolean +from gn_module_monitoring.monitoring.models import PermissionModel, TMonitoringModules + +import logging log = logging.getLogger(__name__) @@ -14,7 +19,6 @@ class MonitoringObject(MonitoringObjectSerializer): def get(self, value=None, field_name=None, depth=0): # par defaut on filtre sur l'id - if not field_name: field_name = self.config_param("id_field_name") if not value: @@ -25,9 +29,7 @@ def get(self, value=None, field_name=None, depth=0): try: Model = self.MonitoringModel() - - req = DB.session.query(Model) - + req = select(Model) # Test pour mettre les relations à joined # if depth > 0: # options = [] @@ -35,9 +37,23 @@ def get(self, value=None, field_name=None, depth=0): # relation_name = children_type + 's' # req = req.options(joinedload(relation_name)) - self._model = req.filter(getattr(Model, field_name) == value).one() + self._model = ( + DB.session.execute(req.where(getattr(Model, field_name) == value)) + .unique() + .scalar_one() + ) self._id = getattr(self._model, self.config_param("id_field_name")) + if isinstance(self._model, PermissionModel) and not isinstance( + self._model, TMonitoringModules + ): + cruved_item_dict = get_objet_with_permission_boolean( + [self._model], + object_code=current_app.config["MONITORINGS"].get("PERMISSION_LEVEL", {})[ + self._object_type + ], + ) + self.cruved = cruved_item_dict[0]["cruved"] return self @@ -68,31 +84,13 @@ def process_synthese(self, process_module=False, limit=1000): return table_name = "v_synthese_{}".format(self._module_code) - try: - import_from_table( - "gn_monitoring", - table_name, - self.config_param("id_field_name"), - self.config_value("id_field_name"), - limit, - ) - except ValueError as e: - # warning - log.warning( - """Error in module monitoring, process_synthese. - Function import_from_table with parameters({}, {}, {}) raises the following error : - {} - """.format( - table_name, - self.config_param("id_field_name"), - self.config_value("id_field_name"), - e, - ) - ) - return {"message": "{}".format(e)}, 500 - except Exception as e: - return {"message": "{}".format(e)}, 500 - + import_from_table( + "gn_monitoring", + table_name, + self.config_param("id_field_name"), + self.config_value("id_field_name"), + limit, + ) return True def create_or_update(self, post_data): @@ -111,10 +109,11 @@ def create_or_update(self, post_data): if b_creation: DB.session.add(self._model) DB.session.commit() - self._id = getattr(self._model, self.config_param("id_field_name")) - self.process_synthese() + # TODO module have synthese enabled + if not post_data["properties"]["id_module"] == "generic": + self.process_synthese() return self @@ -164,11 +163,13 @@ def breadcrumbs(self, params): if params["parents_path"]: object_type = params.get("parents_path", []).pop() - next = MonitoringObject(self._module_code, object_type) - - id_field_name = next.config_param("id_field_name") - next._id = self.get_value(id_field_name) or params.get(id_field_name) - next.get(0) + next = MonitoringObject(self._module_code, object_type, config=self._config) + if next._object_type == "module": + next.get(field_name="module_code", value=self._module_code) + else: + id_field_name = next.config_param("id_field_name") + next._id = self.get_value(id_field_name) or params.get(id_field_name) + next.get(0) else: next = self.get_parent() @@ -201,7 +202,7 @@ def get_list(self, args=None): order_by = args.getlist("order_by") - req = DB.session.query(Model) + req = select(Model) # Traitement de la liste des colonnes à retourner fields_list = args.getlist("fields") @@ -216,7 +217,7 @@ def get_list(self, args=None): for key in args: if hasattr(Model, key) and args[key] not in ["", None, "null", "undefined"]: vals = args.getlist(key) - req = req.filter(getattr(Model, key).in_(vals)) + req = req.where(getattr(Model, key).in_(vals)) # # filtres config @@ -235,7 +236,7 @@ def get_list(self, args=None): # TODO page etc... - res = req.limit(limit).all() + res = DB.session.scalars(req.limit(limit)).all() # patch order by number out = [r.as_dict(fields=fields_list) for r in res] diff --git a/backend/gn_module_monitoring/monitoring/schemas.py b/backend/gn_module_monitoring/monitoring/schemas.py new file mode 100644 index 000000000..2bf98221d --- /dev/null +++ b/backend/gn_module_monitoring/monitoring/schemas.py @@ -0,0 +1,193 @@ +import json +import geojson + +from marshmallow import Schema, fields, validate, post_dump + +from geonature.utils.env import MA +from geonature.core.gn_commons.schemas import MediaSchema, ModuleSchema +from geonature.core.gn_monitoring.models import BibTypeSite +from geonature.core.gn_meta.schemas import DatasetSchema + +from pypnusershub.db.models import User + + +from gn_module_monitoring.monitoring.models import ( + TMonitoringSites, + TMonitoringSitesGroups, + TMonitoringVisits, + TMonitoringModules, + TMonitoringObservations, + TMonitoringObservationDetails, +) + + +def paginate_schema(schema): + class PaginationSchema(Schema): + count = fields.Integer() + limit = fields.Integer() + page = fields.Integer() + items = fields.Nested(schema, many=True, dump_only=True) + + return PaginationSchema + + +class ObserverSchema(MA.SQLAlchemyAutoSchema): + class Meta: + model = User + load_instance = True + exclude = ( + "_password", + "_password_plus", + "active", + "date_insert", + "date_update", + "desc_role", + "email", + "groupe", + "remarques", + "identifiant", + ) + + nom_complet = fields.Str(dump_only=True) + + +class MonitoringBibTypeSiteSchema(MA.SQLAlchemyAutoSchema): + class Meta: + model = BibTypeSite + include_fk = True + + +class MonitoringModuleSchema(MA.SQLAlchemyAutoSchema): + class Meta: + model = TMonitoringModules + load_instance = True + load_relationships = True + include_fk = True + # include_fk=True + + types_site = MA.Pluck(MonitoringBibTypeSiteSchema, "id_nomenclature_type_site", many=True) + datasets = MA.Pluck(DatasetSchema, "id_dataset", many=True) + medias = MA.Nested(MediaSchema, many=True) + + +class MonitoringSitesGroupsSchema(MA.SQLAlchemyAutoSchema): + sites_group_name = fields.String( + validate=validate.Length(min=3, error="Length must be greater than 3"), + ) + + class Meta: + model = TMonitoringSitesGroups + exclude = ("geom_geojson", "geom") + load_instance = True + include_fk = True + load_relationships = True + + medias = MA.Nested(MediaSchema, many=True) + pk = fields.Method("set_pk", dump_only=True) + geometry = fields.Method("serialize_geojson", dump_only=True) + id_digitiser = fields.Method("get_id_digitiser") + is_geom_from_child = fields.Method("set_is_geom_from_child", dump_only=True) + modules = MA.Pluck(ModuleSchema, "id_module", many=True) + + def get_id_digitiser(self, obj): + return obj.id_digitiser + + def set_pk(self, obj): + return "id_sites_group" + + def set_is_geom_from_child(self, obj): + if obj.geom is None and obj.geom_geojson is None: + return True + if obj.geom is not None: + return False + if obj.geom_geojson is not None: + return True + + def serialize_geojson(self, obj): + if obj.geom is not None: + return geojson.dumps(obj.as_geofeature().get("geometry")) + if obj.geom_geojson is not None: + return json.loads(obj.geom_geojson) + + +class MonitoringSitesGroupsDetailSchema(MonitoringSitesGroupsSchema): + modules = MA.Pluck(ModuleSchema, "module_label", many=True) + + +class BibTypeSiteSchema(MA.SQLAlchemyAutoSchema): + label = fields.Method("get_label_from_type_site") + # See if useful in the future: + # type_site = fields.Nested(NomenclatureSchema(only=("label_fr",)), dump_only=True) + + def get_label_from_type_site(self, obj): + return obj.nomenclature.label_fr + + class Meta: + model = BibTypeSite + include_fk = True + load_instance = True + + +class MonitoringSitesSchema(MA.SQLAlchemyAutoSchema): + class Meta: + model = TMonitoringSites + exclude = ("geom_geojson", "geom") + include_fk = True + load_relationships = True + + geometry = fields.Method("serialize_geojson", dump_only=True) + pk = fields.Method("set_pk", dump_only=True) + types_site = MA.Nested(BibTypeSiteSchema, many=True) + id_sites_group = fields.Method("get_id_sites_group") + id_inventor = fields.Method("get_id_inventor") + inventor = fields.Method("get_inventor_name") + medias = MA.Nested(MediaSchema, many=True) + + def serialize_geojson(self, obj): + if obj.geom is not None: + return geojson.dumps(obj.as_geofeature().get("geometry")) + + def set_pk(self, obj): + return "id_base_site" + + def get_id_sites_group(self, obj): + return obj.id_sites_group + + def get_id_inventor(self, obj): + return obj.id_inventor + + def get_inventor_name(self, obj): + if obj.inventor: + return [obj.inventor.nom_complet] + + +class MonitoringVisitsSchema(MA.SQLAlchemyAutoSchema): + class Meta: + model = TMonitoringVisits + include_fk = True + load_relationships = True + + pk = fields.Method("set_pk", dump_only=True) + module = MA.Nested(ModuleSchema) + medias = MA.Nested(MediaSchema, many=True) + + observers = MA.Pluck(ObserverSchema, "id_role", many=True) + + def set_pk(self, obj): + return "id_base_visit" + + +class MonitoringObservationsSchema(MA.SQLAlchemyAutoSchema): + class Meta: + model = TMonitoringObservations + include_fk = True + load_relationships = True + + medias = MA.Nested(MediaSchema, many=True) + + +class MonitoringObservationsDetailsSchema(MA.SQLAlchemyAutoSchema): + class Meta: + model = TMonitoringObservationDetails + include_fk = True + load_relationships = True diff --git a/backend/gn_module_monitoring/monitoring/serializer.py b/backend/gn_module_monitoring/monitoring/serializer.py index 75bb82758..3d0bfaba7 100644 --- a/backend/gn_module_monitoring/monitoring/serializer.py +++ b/backend/gn_module_monitoring/monitoring/serializer.py @@ -1,25 +1,48 @@ """ serialiser """ -import datetime -import uuid -from flask import current_app -from .base import MonitoringObjectBase, monitoring_definitions -from ..utils.utils import to_int -from ..routes.data_utils import id_field_name_dict + +from flask import current_app, g + +from marshmallow import EXCLUDE + from geonature.utils.env import DB from geonature.core.gn_permissions.tools import get_scopes_by_action +from gn_module_monitoring.utils.utils import to_int +from gn_module_monitoring.routes.data_utils import id_field_name_dict +from gn_module_monitoring.utils.routes import get_objet_with_permission_boolean +from gn_module_monitoring.monitoring.models import PermissionModel, TMonitoringModules +from gn_module_monitoring.monitoring.base import MonitoringObjectBase, monitoring_definitions +from gn_module_monitoring.monitoring.schemas import ( + MonitoringModuleSchema, + MonitoringSitesSchema, + MonitoringSitesGroupsSchema, + MonitoringVisitsSchema, + MonitoringObservationsSchema, + MonitoringObservationsDetailsSchema, +) + +MonitoringSerializer_dict = { + "module": MonitoringModuleSchema, # besoin pour retrouver le module depuis module_code à voir si on peux faire sans + "site": MonitoringSitesSchema, + "visit": MonitoringVisitsSchema, + "sites_group": MonitoringSitesGroupsSchema, + "observation": MonitoringObservationsSchema, + "observation_detail": MonitoringObservationsDetailsSchema, +} + class MonitoringObjectSerializer(MonitoringObjectBase): def get_parent(self): parent_type = self.parent_type() - if not parent_type: + + if not parent_type or parent_type == "module": return if not self._parent: self._parent = monitoring_definitions.monitoring_object_instance( - self._module_code, parent_type, self.id_parent() + self._module_code, parent_type, config=self._config, id=self.id_parent() ).get() return self._parent @@ -29,32 +52,83 @@ def get_site_id(self): return if hasattr(self._model, "id_base_site"): return self._model.id_base_site - return - # parent = self.get_parent() - # if not parent: - # return - # return parent.get_site_id() + + parent = self.get_parent() + if not parent: + return + return parent.get_site_id() def as_dict(self, depth): return self._model.as_dict(depth=depth) - def flatten_specific_properties(self, properties): + def flatten_specific_properties(self, properties, only=None): # mise a plat des données spécifiques if "data" not in properties: return + if not only: + only = [] + data = properties.pop("data") data = data if data else {} for attribut_name in self.config_schema(type_schema="specific"): - properties[attribut_name] = data.get(attribut_name) + if attribut_name in only or not only: + properties[attribut_name] = data.get(attribut_name) + # Nécessaire pour récupérer tous les données des champs additionnels mêmes ceux non présents dans la config specific + # Nécessaire pour les récupérer coté frontend lors de l'envoie de l'objet (patch et post) + properties["additional_data_keys"] = [] + for prop in data: + if prop not in properties.keys(): + properties[prop] = data[prop] + properties["additional_data_keys"].append(prop) def unflatten_specific_properties(self, properties): data = {} - for attribut_name in self.config_schema("specific"): - val = properties.pop(attribut_name) - data[attribut_name] = val + for attribut_name, attribut_value in self.config_schema("specific").items(): + if "type_widget" in attribut_value and attribut_value["type_widget"] != "html": + if attribut_name in properties: + val = properties.pop(attribut_name) + else: + # TODO [DEV-SUIVI-EOLIEN] évaluer l'incidence + # TODO [DEV-SUIVI-EOLIEN] Ici il faut exclure les properties liés à la config generic + aux propriétés du model + # voir comment générer les proprités spécifiques + # non définies dans le schéma + val = None + data[attribut_name] = val if data: properties["data"] = data + else: + properties["data"] = {} + # On ajoute les propriétés associées aux types de site qui ne sont ni dans le schema specific ni dans generic ou appartenant au modèle + prop_remaining_to_check = list(properties.keys()) + + for prop in prop_remaining_to_check: + is_in_model = hasattr(self._model, prop) + if ( + not is_in_model + and prop not in self.config_schema("generic").keys() + and prop != "id_module" + ): + properties["data"][prop] = properties.pop(prop) + + def get_readable_list_object(self, relation_name, children_type): + childs_model = monitoring_definitions.MonitoringModel(object_type=children_type) + + if getattr(childs_model, "has_instance_permission"): + scope = get_scopes_by_action( + id_role=g.current_user.id_role, + module_code=self._module_code, + object_code=current_app.config["MONITORINGS"].get("PERMISSION_LEVEL", {})[ + children_type + ], + )["R"] + childs_model = [ + m for m in getattr(self._model, relation_name) if m.has_instance_permission(scope) + ] + return childs_model + else: + childs_model = getattr(self._model, relation_name) + return childs_model def serialize_children(self, depth): children_types = self.config_param("children_types") @@ -73,22 +147,46 @@ def serialize_children(self, depth): children_of_type = [] - for child_model in getattr(self._model, relation_name): + childs_object_readable = self.get_readable_list_object( + relation_name, children_type=children_type + ) + for child_model in childs_object_readable: child = monitoring_definitions.monitoring_object_instance( - self._module_code, children_type, model=child_model + self._module_code, children_type, config=self._config, model=child_model ) - children_of_type.append(child.serialize(depth)) + children_of_type.append(child.serialize(depth, is_child=True)) children[children_type] = children_of_type return children + def get_cruved_by_object(self): + list_model = [] + list_model.append(self._model) + if ( + isinstance(list_model[0], PermissionModel) + and not isinstance(list_model[0], TMonitoringModules) + and self._module_code != "generic" + ): + id_name = list_model[0].get_id_name() + cruved_item_dict = get_objet_with_permission_boolean( + list_model, + module_code=self._module_code, + object_code=current_app.config["MONITORINGS"].get("PERMISSION_LEVEL", {})[ + self._object_type + ], + ) + for cruved_item in cruved_item_dict: + if self._id == cruved_item[id_name]: + self.cruved = cruved_item["cruved"] + return self.cruved + def properties_names(self): generic = list(self.config_schema("generic").keys()) data = ["data"] if hasattr(self._model, "data") else [] return generic + data - def serialize(self, depth=1): + def serialize(self, depth=1, is_child=False): # TODO faire avec un as_dict propre (avec props et relationships) if depth is None: depth = 1 @@ -101,43 +199,75 @@ def serialize(self, depth=1): return None self._model = Model() + # Liste des propriétés de l'objet qui doivent être récupérées + display_properties = [] + # Liste des propriétés spécifique de l'objet qui doivent être récupérées + display_specific = [] + if is_child: + module_config = self.config() + # Si l'objet est un enfant on ne serialize que les attributs utilisés dans les data list + display_properties = module_config[self._object_type]["display_list"] + # liste des propriétés "génériques" + display_generic = [ + k + for k in display_properties + if k in module_config[self._object_type]["generic"].keys() + ] + # liste des propriétés "spécifique" + display_specific = [ + k + for k in display_properties + if k in module_config[self._object_type]["specific"].keys() + ] + + display_generic.append("data") + display_generic.append(self.config_param("id_field_name")) - properties = {} + # Sérialisation de l'objet + dump_object = MonitoringSerializer_dict[self._object_type]( + unknown=EXCLUDE, only=display_generic + ).dump(self._model) - for field_name in self.properties_names(): - # val = self._model.__dict__.get(field_name) - val = getattr(self._model, field_name) - if isinstance(val, (datetime.date, uuid.UUID)): - val = str(val) - properties[field_name] = val + else: + # Si l'objet n'est pas un enfant on récupére toutes les informations + # Pour pourvoir afficher le détails + dump_object = MonitoringSerializer_dict[self._object_type](unknown=EXCLUDE).dump( + self._model + ) + properties = dump_object + + # Extraction des proprités spécifiques au même niveau que les génériques + self.flatten_specific_properties(properties, only=display_specific) + # Sérialisation des enfants children = None if depth >= 0: children = self.serialize_children(depth) - # processe properties - self.flatten_specific_properties(properties) - schema = self.config_schema() + for key in schema: - definition = schema[key] - value = properties[key] - if not isinstance(value, list): - continue + if key in properties: + definition = schema[key] + value = properties[key] + if not isinstance(value, list): + continue + + type_util = definition.get("type_util") + + # on passe d'une list d'objet à une liste d'id + # si type_util est defini pour ce champs + # si on a bien affaire à une liste de modèles sqla + properties[key] = [ + ( + getattr(v, id_field_name_dict[type_util]) + if (isinstance(v, DB.Model) and type_util) + else v.as_dict() if (isinstance(v, DB.Model) and not type_util) else v + ) + for v in value + ] - type_util = definition.get("type_util") - - # on passe d'une list d'objet à une liste d'id - # si type_util est defini pour ce champs - # si on a bien affaire à une liste de modèles sqla - properties[key] = [ - getattr(v, id_field_name_dict[type_util]) - if (isinstance(v, DB.Model) and type_util) - else v.as_dict() - if (isinstance(v, DB.Model) and not type_util) - else v - for v in value - ] + properties["id_parent"] = to_int(self.id_parent()) monitoring_object_dict = { "properties": properties, @@ -145,9 +275,9 @@ def serialize(self, depth=1): "module_code": self._module_code, "site_id": self.get_site_id(), "id": self._id, + "cruved": self.get_cruved_by_object(), } - properties["id_parent"] = to_int(self.id_parent()) if children: monitoring_object_dict["children"] = children @@ -166,10 +296,20 @@ def populate(self, post_data): self.unflatten_specific_properties(properties) # pretraitement (pour t_base_site et cor_site_module) - self.preprocess_data(properties) + if "dataComplement" in post_data: + self.preprocess_data(properties, post_data["dataComplement"]) + else: + self.preprocess_data(properties) # ajout des données en base - if hasattr(self._model, "from_geofeature"): + if ( + hasattr(self._model, "from_geofeature") + and not (len(list(post_data)) == 1 and list(post_data)[0] == "properties") + and post_data["geometry"] is not None + ): + for key in list(post_data): + if key not in ("properties", "geometry", "type"): + post_data.pop(key) self._model.from_geofeature(post_data, True) else: self._model.from_dict(properties, True) diff --git a/backend/gn_module_monitoring/monitoring/utils.py b/backend/gn_module_monitoring/monitoring/utils.py index e69de29bb..05f0dc18b 100644 --- a/backend/gn_module_monitoring/monitoring/utils.py +++ b/backend/gn_module_monitoring/monitoring/utils.py @@ -0,0 +1,10 @@ +import json + +from jinja2.utils import markupsafe + + +def json_formatter(view, context, model, name): + """Prettify JSON data in flask admin lists""" + value = getattr(model, name) + json_value = json.dumps(value, ensure_ascii=False, indent=2) + return markupsafe.Markup("
{}
".format(json_value)) diff --git a/backend/gn_module_monitoring/routes/config.py b/backend/gn_module_monitoring/routes/config.py index f645f8cde..332c606dd 100644 --- a/backend/gn_module_monitoring/routes/config.py +++ b/backend/gn_module_monitoring/routes/config.py @@ -1,9 +1,7 @@ -from flask import request - from utils_flask_sqla.response import json_resp -from ..blueprint import blueprint -from ..config.repositories import get_config, get_config_frontend +from gn_module_monitoring.blueprint import blueprint +from gn_module_monitoring.config.repositories import get_config @blueprint.route("/config/", methods=["GET"]) @@ -13,5 +11,5 @@ def get_config_api(module_code): """ route qui renvoie la config pour un module donné """ - - return get_config_frontend(module_code, force=True) + config = get_config(module_code, force=True) + return dict(config) diff --git a/backend/gn_module_monitoring/routes/data_utils.py b/backend/gn_module_monitoring/routes/data_utils.py index 6f840827b..a398d2260 100644 --- a/backend/gn_module_monitoring/routes/data_utils.py +++ b/backend/gn_module_monitoring/routes/data_utils.py @@ -8,33 +8,38 @@ """ from flask import request -from sqlalchemy import and_, inspect, cast -from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound -from pypnnomenclature.models import TNomenclatures, BibNomenclaturesTypes -from pypnnomenclature.repository import get_nomenclature_list +from sqlalchemy import and_, inspect, cast, select +from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound -# from geonature.core.taxonomie.models import Taxref, BibListes +from geonature.utils.env import DB from geonature.core.users.models import VUserslistForallMenu - +from geonature.core.gn_meta.models import TDatasets +from geonature.utils.errors import GeoNatureError +from geonature.core.gn_monitoring.models import BibTypeSite +from geonature.core.gn_commons.models import TModules from pypnusershub.db.models import User, UserList -from pypn_habref_api.models import Habref -from apptax.taxonomie.models import Taxref, BibListes -from utils_flask_sqla.response import json_resp +from pypnnomenclature.models import TNomenclatures, BibNomenclaturesTypes +from pypnnomenclature.repository import get_nomenclature_list -from geonature.core.gn_meta.models import TDatasets -from ref_geo.models import LAreas, LiMunicipalities -from geonature.utils.env import DB +from apptax.taxonomie.models import Taxref, BibListes -from geonature.utils.errors import GeoNatureError +from utils_flask_sqla.response import json_resp -from ..blueprint import blueprint +from pypn_habref_api.models import Habref -from ..config.repositories import get_config +from ref_geo.models import LAreas, LiMunicipalities -from ..monitoring.models import TMonitoringSitesGroups, TMonitoringSites +from gn_module_monitoring.blueprint import blueprint +from gn_module_monitoring.config.repositories import get_config +from gn_module_monitoring.utils.routes import get_sites_groups_from_module_id +from gn_module_monitoring.monitoring.schemas import MonitoringSitesGroupsSchema +from gn_module_monitoring.monitoring.models import ( + TMonitoringSites, + TMonitoringSitesGroups, +) model_dict = { "habitat": Habref, @@ -42,12 +47,14 @@ "user": User, "taxonomy": Taxref, "dataset": TDatasets, + "types_site": BibTypeSite, "observer_list": UserList, "taxonomy_list": BibListes, "sites_group": TMonitoringSitesGroups, "site": TMonitoringSites, "area": LAreas, "municipality": LiMunicipalities, + "module": TModules, } @@ -66,11 +73,11 @@ def get_init_data(module_code): """ renvoie les données nomenclatures, etc à précharger par le module """ - + if module_code == "MONITORINGS": + module_code = "generic" out = {} config = get_config(module_code, True) data = config.get("data") - if not data: return {} @@ -87,19 +94,22 @@ def get_init_data(module_code): # user if data.get("user"): - res_user = DB.session.query(VUserslistForallMenu).filter_by(id_menu=data.get("user")).all() + res_user = DB.session.scalars( + select(VUserslistForallMenu).where(VUserslistForallMenu.id_menu == data.get("user")) + ).all() out["user"] = [user.as_dict() for user in res_user] # sites_group if "sites_group" in config: - res_sites_group = ( - DB.session.query(TMonitoringSitesGroups).filter_by(id_module=id_module).all() - ) - out["sites_group"] = [sites_group.as_dict() for sites_group in res_sites_group] + sites_groups = get_sites_groups_from_module_id(id_module) + schema = MonitoringSitesGroupsSchema() + out["sites_group"] = [schema.dump(sites_group) for sites_group in sites_groups] # dataset (cruved ??) res_dataset = ( - DB.session.query(TDatasets).filter(TDatasets.modules.any(module_code=module_code)).all() + DB.session.scalars(select(TDatasets).where(TDatasets.modules.any(module_code=module_code))) + .unique() + .all() ) out["dataset"] = [dataset.as_dict() for dataset in res_dataset] @@ -128,10 +138,10 @@ def get_util_nomenclature_api(code_nomenclature_type, cd_nomenclature): if not hasattr(TNomenclatures, field_name) and field_name != "all": raise GeoNatureError("TNomenclatures n'a pas de champs {}".format(field_name)) + # requête try: - scope = TNomenclatures if field_name == "all" else getattr(TNomenclatures, field_name) - res = ( - DB.session.query(scope) + res = DB.session.execute( + select(TNomenclatures) .join( BibNomenclaturesTypes, and_( @@ -139,12 +149,19 @@ def get_util_nomenclature_api(code_nomenclature_type, cd_nomenclature): BibNomenclaturesTypes.mnemonique == code_nomenclature_type, ), ) - .filter(TNomenclatures.cd_nomenclature == cd_nomenclature) - .one() + .where(TNomenclatures.cd_nomenclature == cd_nomenclature) + ).scalar_one() + + return ( + res.as_dict() + if field_name == "all" + else res.as_dict( + fields=[ + field_name, + ] + ) ) - return res.as_dict() if field_name == "all" else res[0] - except MultipleResultsFound: raise GeoNatureError( "Nomenclature : multiple results for given type {} and code {}".format( @@ -189,65 +206,25 @@ def get_util_from_id_api(type_util, id): if not obj or not id_field_name: return None - scope = obj if field_name == "all" else getattr(obj, field_name) # requête try: res = ( - DB.session.query(scope) - .filter(cast(getattr(obj, id_field_name), DB.String) == id) - .one() + DB.session.execute( + select(obj).where(cast(getattr(obj, id_field_name), DB.String) == id) + ) + .unique() + .scalar_one() ) - return res.as_dict() if field_name == "all" else res[0] + return ( + res.as_dict() + if field_name == "all" + else res.as_dict( + fields=[ + field_name, + ] + ) + ) except NoResultFound: raise GeoNatureError("{} : no results found for id {}".format(type_util, id)) - - -@blueprint.route("util//", methods=["GET"]) -@json_resp -def get_util_from_ids_api(type_util, ids): - """ - variante de get_util_from_id_api pour plusieurs id - renvoie un tableau de valeur (ou de dictionnaire si key est 'all') - - parametre get - key: all renvoie tout l'objet - sinon renvoie un champ - separator_out: - pour reformer une chaine de caractere a partir du tableau résultat de la requete - si separator_out == ' ,' - alors ['jean', 'pierre', 'paul'].join(separator_out) -> 'jean, pierre, paul' - - :param type_util: 'nomenclature' | 'taxonomy' | 'utilisateur' - :param ids: plusieurs id reliée par des '-' (ex: 1-123-3-4) - :type type_util: str - :type ids: str - :return list si key=all ou chaine de caratere - - """ - - field_name = request.args.get("field_name", "all") - separator_out = request.args.get("sep_out", ", ") - - # tableau d'id depuis ids - list_ids = list(ids.split("-")) - - obj = model_dict.get(type_util) - id_field_name = id_field_name_dict.get(type_util) - - if not hasattr(obj, field_name) and field_name != "all": - raise GeoNatureError("{} n'a pas de champs {}".format(type_util, field_name)) - - # requête - scope = obj if field_name == "all" else getattr(obj, field_name) - res = DB.session.query(scope).filter(getattr(obj, id_field_name).in_(list_ids)).all() - - if len(res) != len(list_ids): - raise GeoNatureError("{} : pas toutes les id trouvées parmis {}".format(type_util, ids)) - - if field_name == "all": - return [r.as_dict() for r in res] - - # renvoie une chaine de caratère - return separator_out.join([r[0] for r in res]) diff --git a/backend/gn_module_monitoring/routes/modules.py b/backend/gn_module_monitoring/routes/modules.py index c12d7d3fa..750c2e3b9 100644 --- a/backend/gn_module_monitoring/routes/modules.py +++ b/backend/gn_module_monitoring/routes/modules.py @@ -5,18 +5,21 @@ from flask import request from utils_flask_sqla.response import json_resp_accept_empty_list, json_resp -from ..blueprint import blueprint -from ..utils.utils import to_int - -from geonature.core.gn_permissions.tools import get_scopes_by_action +from geonature.core.gn_permissions.tools import get_scopes_by_action, has_any_permissions_by_action from geonature.core.gn_permissions.decorators import check_cruved_scope from gn_module_monitoring import MODULE_CODE - -from ..modules.repositories import ( +from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema +from gn_module_monitoring.blueprint import blueprint +from gn_module_monitoring.modules.repositories import ( get_module, get_modules, ) +from gn_module_monitoring.utils.utils import to_int +from gn_module_monitoring.utils.routes import ( + query_all_types_site_from_module_id, + get_object_list_monitorings, +) @blueprint.route("/module/", methods=["GET"]) @@ -44,6 +47,23 @@ def get_module_api(value): return module_out +@blueprint.route("/cruved_object", methods=["GET"]) +@check_cruved_scope("R", module_code=MODULE_CODE) +def get_cruved_monitorings(): + """ + Renvoie la liste des modules de suivi + """ + dic_object_cruved = {} + object_list_tuples = get_object_list_monitorings() + object_list = [value for (value,) in object_list_tuples] + for object in object_list: + dic_object_cruved[object] = has_any_permissions_by_action( + module_code=MODULE_CODE, object_code=object + ) + + return dic_object_cruved + + @blueprint.route("/modules", methods=["GET"]) @check_cruved_scope("R", module_code=MODULE_CODE) @json_resp_accept_empty_list @@ -65,3 +85,15 @@ def get_modules_api(): modules_out.append(module_out) return modules_out + + +# TODEL ? +@blueprint.route("/modules//types_sites", methods=["GET"]) +def get_all_types_site_from_module_id(module_code): + module = get_module("module_code", module_code) + id_module = None + if module: + id_module = module.id_module + types_site = query_all_types_site_from_module_id(id_module) + schema = BibTypeSiteSchema() + return [schema.dump(res) for res in types_site] diff --git a/backend/gn_module_monitoring/routes/monitoring.py b/backend/gn_module_monitoring/routes/monitoring.py index 0eca32a6b..ee3863f65 100644 --- a/backend/gn_module_monitoring/routes/monitoring.py +++ b/backend/gn_module_monitoring/routes/monitoring.py @@ -3,44 +3,47 @@ site, visit, observation, ... """ - -from pathlib import Path -from werkzeug.exceptions import NotFound -from flask import request, send_from_directory, url_for, g, current_app import datetime as dt -from sqlalchemy.orm import joinedload +from werkzeug.exceptions import Forbidden -from utils_flask_sqla.response import json_resp, json_resp_accept_empty_list -from utils_flask_sqla.response import to_csv_resp, to_json_resp -from utils_flask_sqla_geo.generic import GenericTableGeo -from utils_flask_sqla.generic import serializeQuery +from flask import request, url_for, g, current_app +from sqlalchemy import select +from sqlalchemy.orm import joinedload -from ..blueprint import blueprint +from utils_flask_sqla.response import json_resp, json_resp_accept_empty_list +from utils_flask_sqla.response import to_csv_resp +from utils_flask_sqla_geo.generic import GenericQueryGeo +from geonature.core.gn_permissions import decorators as permissions from geonature.core.gn_permissions.decorators import check_cruved_scope from geonature.core.gn_commons.models.base import TModules -from geonature.core.gn_permissions.models import TObjects, Permission +from geonature.core.gn_permissions.models import TObjects from geonature.utils.env import DB, ROOT_DIR import geonature.utils.filemanager as fm +from gn_module_monitoring.blueprint import blueprint from gn_module_monitoring import MODULE_CODE -from ..monitoring.definitions import monitoring_definitions -from ..modules.repositories import get_module -from ..utils.utils import to_int -from ..config.repositories import get_config +from gn_module_monitoring.monitoring.definitions import monitoring_definitions +from gn_module_monitoring.modules.repositories import get_module +from gn_module_monitoring.utils.utils import to_int +from gn_module_monitoring.config.repositories import get_config @blueprint.url_value_preprocessor def set_current_module(endpoint, values): # recherche du sous-module courrant requested_module_code = values.get("module_code") or MODULE_CODE - current_module = ( - TModules.query.options(joinedload(TModules.objects)) - .filter_by(module_code=requested_module_code) - .first_or_404(f"No module with code {requested_module_code} {endpoint}") + if requested_module_code == "generic": + requested_module_code = "MONITORINGS" + + current_module = DB.first_or_404( + statement=select(TModules) + .options(joinedload(TModules.objects)) + .where(TModules.module_code == requested_module_code), + description=f"No module with code {requested_module_code} {endpoint}", ) g.current_module = current_module @@ -56,12 +59,12 @@ def set_current_module(endpoint, values): return # Test si l'object de permission existe - requested_permission_object = TObjects.query.filter_by( - code_object=requested_permission_object_code - ).first_or_404( - f"No permission object with code {requested_permission_object_code} {endpoint}" + requested_permission_object = DB.first_or_404( + statement=select(TObjects).where( + TObjects.code_object == requested_permission_object_code + ), + description=f"No permission object with code {requested_permission_object_code} {endpoint}", ) - # si l'object de permission est associé au module => il devient l'objet courant # - sinon se sera 'ALL' par defaut for module_perm_object in current_module.objects: @@ -71,17 +74,15 @@ def set_current_module(endpoint, values): @blueprint.route("/object///", methods=["GET"]) -@blueprint.route( - "/object//", defaults={"id": None}, methods=["GET"] -) +@blueprint.route("/object//", methods=["GET"]) @blueprint.route( "/object/module", - defaults={"module_code": None, "object_type": "module", "id": None}, methods=["GET"], ) @check_cruved_scope("R") @json_resp -def get_monitoring_object_api(module_code, object_type, id): +@permissions.check_cruved_scope("R", get_scope=True) +def get_monitoring_object_api(scope, module_code=None, object_type="module", id=None): """ renvoie un object, à partir de type de l'object et de son id @@ -98,20 +99,27 @@ def get_monitoring_object_api(module_code, object_type, id): # field_name = param.get('field_name') # value = module_code if object_type == 'module' - get_config(module_code, force=True) depth = to_int(request.args.get("depth", 1)) + config = get_config(module_code, force=True) + + monitoring_obj = monitoring_definitions.monitoring_object_instance( + module_code, object_type, config=config, id=id + ) + if id != None: + object = monitoring_obj.get(depth=depth) + if not object._model.has_instance_permission(scope=scope): + raise Forbidden(f"User {g.current_user} cannot read {object_type} {object._id}") + return ( - monitoring_definitions.monitoring_object_instance(module_code, object_type, id).get( - depth=depth - ) + monitoring_obj.get(depth=depth) # .get(value=value, field_name = field_name) .serialize(depth) ) -def create_or_update_object_api(module_code, object_type, id): +def create_or_update_object_api(module_code, object_type, id=None): """ route pour la création ou la modification d'un objet si id est renseigné, c'est une création (PATCH) @@ -130,18 +138,55 @@ def create_or_update_object_api(module_code, object_type, id): # recupération des données post post_data = dict(request.get_json()) - module = get_module("module_code", module_code) - # on rajoute id_module s'il n'est pas renseigné par défaut ?? - post_data["properties"]["id_module"] = module.id_module + # on rajoute id_module s'il n'est pas renseigné par défaut + post_data["properties"].setdefault("id_module", None) + if not post_data["properties"]["id_module"]: + if module_code != "generic": + post_data["properties"]["id_module"] = get_module("module_code", module_code).id_module + else: + post_data["properties"]["id_module"] = "generic" + config = get_config(module_code, force=True) return ( - monitoring_definitions.monitoring_object_instance(module_code, object_type, id) + monitoring_definitions.monitoring_object_instance( + module_code, object_type, config=config, id=id + ) .create_or_update(post_data) .serialize(depth) ) +def get_serialized_object(module_code, object_type, id): + """ + renvoie un object, à partir de type de l'object et de son id + + :param module_code: reference le module concerne + :param object_type: le type d'object (site, visit, obervation) + :param id : l'identifiant de l'object (de id_base_site pour site) + :type module_code: str + :type object_type: str + :type id: int + + :return: renvoie l'object requis + :rtype: dict + """ + + # field_name = param.get('field_name') + # value = module_code if object_type == 'module' + config = get_config(module_code, force=True) + + depth = to_int(request.args.get("depth", 1)) + + return ( + monitoring_definitions.monitoring_object_instance( + module_code, object_type, config=config, id=id + ).get(depth=depth) + # .get(value=value, field_name = field_name) + .serialize(depth) + ) + + # update object @blueprint.route("object///", methods=["PATCH"]) @blueprint.route( @@ -151,8 +196,19 @@ def create_or_update_object_api(module_code, object_type, id): ) @check_cruved_scope("U") @json_resp -def update_object_api(module_code, object_type, id): - get_config(module_code, force=True) +@permissions.check_cruved_scope("U", get_scope=True) +def update_object_api(scope, module_code, object_type, id): + depth = to_int(request.args.get("depth", 1)) + if id != None: + + config = get_config(module_code, force=True) + object = monitoring_definitions.monitoring_object_instance( + module_code, object_type, config=config, id=id + ).get(depth=depth) + if not object._model.has_instance_permission(scope=scope): + raise Forbidden(f"User {g.current_user} cannot update {object_type} {object._id}") + + post_data = dict(request.get_json()) return create_or_update_object_api(module_code, object_type, id) @@ -168,7 +224,8 @@ def update_object_api(module_code, object_type, id): @check_cruved_scope("C") @json_resp def create_object_api(module_code, object_type, id): - get_config(module_code, force=True) + post_data = dict(request.get_json()) + # get_config(module_code, force=True) return create_or_update_object_api(module_code, object_type, id) @@ -181,10 +238,27 @@ def create_object_api(module_code, object_type, id): ) @check_cruved_scope("D") @json_resp -def delete_object_api(module_code, object_type, id): - get_config(module_code, force=True) +@permissions.check_cruved_scope("D", get_scope=True) +def delete_object_api(scope, module_code, object_type, id): + depth = to_int(request.args.get("depth", 1)) - return monitoring_definitions.monitoring_object_instance(module_code, object_type, id).delete() + # ??? PLUS VALABLE + # NOTE: normalement on ne peut plus supprimer les groupes de site / sites par l'entrée protocoles + # if object_type in ("site", "sites_group"): + # raise Exception( + # f"No right to delete {object_type} from protocol. The {object_type} with id: {id} could be linked with others protocols" + # ) + + config = get_config(module_code=module_code, force=True) + monitoring_obj = monitoring_definitions.monitoring_object_instance( + module_code, object_type, config=config, id=id + ) + if id != None: + object = monitoring_obj.get(depth=depth) + if not object._model.has_instance_permission(scope=scope): + raise Forbidden(f"User {g.current_user} cannot delete {object_type} {object._id}") + + return monitoring_obj.delete() # breadcrumbs @@ -200,11 +274,13 @@ def delete_object_api(module_code, object_type, id): @check_cruved_scope("R") @json_resp def breadcrumbs_object_api(module_code, object_type, id): - get_config(module_code, force=True) query_params = dict(**request.args) query_params["parents_path"] = request.args.getlist("parents_path") + config = get_config(module_code=module_code, force=True) return ( - monitoring_definitions.monitoring_object_instance(module_code, object_type, id) + monitoring_definitions.monitoring_object_instance( + module_code, object_type, config=config, id=id + ) .get() .breadcrumbs(query_params) ) @@ -215,11 +291,11 @@ def breadcrumbs_object_api(module_code, object_type, id): @check_cruved_scope("R") @json_resp_accept_empty_list def list_object_api(module_code, object_type): - get_config(module_code, force=True) + config = get_config(module_code, force=True) - return monitoring_definitions.monitoring_object_instance(module_code, object_type).get_list( - request.args - ) + return monitoring_definitions.monitoring_object_instance( + module_code, object_type, config=config + ).get_list(request.args) # mise à jour de la synthèse @@ -227,10 +303,10 @@ def list_object_api(module_code, object_type): @check_cruved_scope("U", object_code="MONITORINGS_MODULES") @json_resp def update_synthese_api(module_code): - get_config(module_code, force=True) + config = get_config(module_code, force=True) return ( - monitoring_definitions.monitoring_object_instance(module_code, "module") + monitoring_definitions.monitoring_object_instance(module_code, "module", config=config) .get() .process_synthese(process_module=True) ) @@ -253,25 +329,35 @@ def export_all_observations(module_code, method): :returns: Array of dict """ id_dataset = request.args.get("id_dataset", None, int) + try: + export = GenericQueryGeo( + DB=DB, + tableName=f"v_export_{module_code.lower()}_{method}", + schemaName="gn_monitoring", + filters=[], + limit=50000, + offset=0, + geometry_field=None, + srid=None, + ) + except KeyError: + return f"table v_export_{module_code.lower()}_{method} doesn't exist", 404 - view = GenericTableGeo( - tableName=f"v_export_{module_code.lower()}_{method}", - schemaName="gn_monitoring", - engine=DB.engine, - ) - columns = view.tableDef.columns - q = DB.session.query(*columns) - # Filter with dataset if is set - if hasattr(columns, "id_dataset") and id_dataset: - q = q.filter(columns.id_dataset == id_dataset) - data = q.all() + model = export.get_model() + columns = export.view.tableDef.columns + schema = export.get_marshmallow_schema() + q = select(model) + # Filter with dataset if is set + if hasattr(model, "id_dataset") and id_dataset: + q = q.where(getattr(model, "id_dataset") == id_dataset) + + data = DB.session.scalars(q).all() timestamp = dt.datetime.now().strftime("%Y_%m_%d_%Hh%Mm%S") filename = f"{module_code}_{method}_{timestamp}" - return to_csv_resp( filename, - data=serializeQuery(data, q.column_descriptions), + data=schema().dump(data, many=True), separator=";", columns=[ db_col.key for db_col in columns if db_col.key != "geom" @@ -289,8 +375,11 @@ def post_export_pdf(module_code, object_type, id): """ depth = to_int(request.args.get("depth", 0)) + config = get_config(module_code, force=True) monitoring_object = ( - monitoring_definitions.monitoring_object_instance(module_code, object_type, id) + monitoring_definitions.monitoring_object_instance( + module_code, object_type, config=config, id=id + ) .get() .serialize(depth) ) diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py new file mode 100644 index 000000000..8103e35b4 --- /dev/null +++ b/backend/gn_module_monitoring/routes/site.py @@ -0,0 +1,273 @@ +import json + +from flask import request, g +from flask.json import jsonify + +from sqlalchemy import and_, select +from sqlalchemy.orm import Load, joinedload +from sqlalchemy.sql import func +from werkzeug.datastructures import MultiDict +from werkzeug.exceptions import Forbidden + +from geonature.utils.env import db +from geonature.core.gn_commons.schemas import ModuleSchema +from geonature.core.gn_monitoring.models import BibTypeSite +from geonature.core.gn_permissions import decorators as permissions +from geonature.core.gn_permissions.decorators import check_cruved_scope + +from pypnnomenclature.models import TNomenclatures + +from gn_module_monitoring import MODULE_CODE +from gn_module_monitoring.blueprint import blueprint +from gn_module_monitoring.config.repositories import get_config +from gn_module_monitoring.monitoring.models import ( + TMonitoringModules, + TMonitoringSites, +) +from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema +from gn_module_monitoring.routes.monitoring import ( + create_or_update_object_api, + get_serialized_object, +) +from gn_module_monitoring.routes.modules import get_modules +from gn_module_monitoring.utils.routes import ( + filter_params, + geojson_query, + get_limit_page, + get_sort, + paginate, + paginate_scope, + sort, + query_all_types_site_from_site_id, + filter_according_to_column_type_for_site, + sort_according_to_column_type_for_site, + get_objet_with_permission_boolean, +) + + +@blueprint.route("/sites/config", methods=["GET"]) +def get_config_sites(id=None, module_code="generic", object_type="site"): + # A QUOI SERT CETTE ROUTE ? + obj = get_serialized_object(module_code, object_type, id) + return obj["properties"] + + +@blueprint.route("/sites/types", methods=["GET"]) +def get_types_site(): + params = MultiDict(request.args) + limit, page = get_limit_page(params=params) + sort_label, sort_dir = get_sort( + params=params, default_sort="id_nomenclature_type_site", default_direction="desc" + ) + + query = filter_params(BibTypeSite, query=select(BibTypeSite), params=params) + query = sort(query=query, model=BibTypeSite, sort=sort_label, sort_dir=sort_dir) + + return paginate( + query=query, + schema=BibTypeSiteSchema, + limit=limit, + page=page, + ) + + +@blueprint.route("/sites/types/label", methods=["GET"]) +def get_types_site_by_label(): + params = MultiDict(request.args) + limit, page = get_limit_page(params=params) + sort_label, sort_dir = get_sort( + params=params, default_sort="label_fr", default_direction="desc" + ) + + query = ( + select(BibTypeSite) + .join(BibTypeSite.nomenclature) + .where(TNomenclatures.label_fr.ilike(f"%{params['label_fr']}%")) + ) + if sort_dir == "asc": + query = query.order_by(TNomenclatures.label_fr.asc()) + + # See if there are not too much labels since they are used + # in select in the frontend side. And an infinite select is not + # implemented + return paginate( + query=query, + schema=BibTypeSiteSchema, + limit=limit, + page=page, + ) + + +@blueprint.route("/sites/types/", methods=["GET"]) +def get_type_site_by_id(id_type_site): + res = db.get_or_404(BibTypeSite, id_type_site) + + schema = BibTypeSiteSchema() + return schema.dump(res) + + +@blueprint.route("/sites//types", methods=["GET"], defaults={"object_type": "site"}) +def get_all_types_site_from_site_id(id_site, object_type): + types_site = query_all_types_site_from_site_id(id_site) + schema = BibTypeSiteSchema() + return [schema.dump(res) for res in types_site] + + +@blueprint.route("/sites", methods=["GET"], defaults={"object_type": "site"}) +@check_cruved_scope("R", module_code=MODULE_CODE, object_code="MONITORINGS_SITES") +def get_sites(object_type): + object_code = "MONITORINGS_SITES" + params = MultiDict(request.args) + # TODO: add filter support + limit, page = get_limit_page(params=params) + sort_label, sort_dir = get_sort( + params=params, default_sort="id_base_site", default_direction="desc" + ) + + query = select(TMonitoringSites) + query = filter_according_to_column_type_for_site(query, params) + query = sort_according_to_column_type_for_site(query, sort_label, sort_dir) + + query_allowed = TMonitoringSites.filter_by_readable(query=query, object_code=object_code) + return paginate_scope( + query=query_allowed, + schema=MonitoringSitesSchema, + limit=limit, + page=page, + object_code=object_code, + ) + + +@blueprint.route("/sites/", methods=["GET"], defaults={"object_type": "site"}) +@permissions.check_cruved_scope( + "R", get_scope=True, module_code=MODULE_CODE, object_code="MONITORINGS_SITES" +) +def get_site_by_id(scope, id, object_type): + site = db.get_or_404(TMonitoringSites, id) + if not site.has_instance_permission(scope=scope): + raise Forbidden(f"User {g.current_user} cannot read site {site.id_base_site}") + schema = MonitoringSitesSchema() + response = schema.dump(site) + response["cruved"] = get_objet_with_permission_boolean( + [site], object_code="MONITORINGS_SITES" + )[0]["cruved"] + response["geometry"] = json.loads(response["geometry"]) + return response + + +@blueprint.route("/sites/geometries", methods=["GET"], defaults={"object_type": "site"}) +@check_cruved_scope("R", module_code=MODULE_CODE, object_code="MONITORINGS_SITES") +def get_all_site_geometries(object_type): + object_code = "MONITORINGS_SITES" + # params = request.args.to_dict(flat=True) + params = dict(**request.args) + types_site = [] + if "types_site" in params: + types_site = request.args.getlist("types_site") + if not types_site[0].isdigit(): + # HACK gestionnaire des sites + # Quand filtre sur type de site envoie une chaine de caractère + params["types_site_label"] = types_site[0] + params.pop("types_site") + types_site = None + else: + params["types_site"] = types_site + + query = select(TMonitoringSites) + query_allowed = TMonitoringSites.filter_by_readable(query=query, object_code=object_code) + query_allowed = query_allowed.with_only_columns( + TMonitoringSites.id_base_site, + TMonitoringSites.base_site_name, + TMonitoringSites.geom, + TMonitoringSites.id_sites_group, + ).distinct() + query_allowed = TMonitoringSites.filter_by_params(query=query_allowed, params=params) + + if types_site: + query_allowed = TMonitoringSites.filter_by_specific( + query=query_allowed, id_types_site=types_site, params=params + ) + subquery = query_allowed.subquery() + result = geojson_query(subquery) + + return jsonify(result) + + +@blueprint.route("/sites//modules", methods=["GET"]) +@check_cruved_scope("R", module_code=MODULE_CODE, object_code="MONITORINGS_SITES") +def get_module_by_id_base_site(id_base_site: int): + modules_object = get_modules() + modules = get_objet_with_permission_boolean( + modules_object, object_code="MONITORINGS_VISITES", depth=0 + ) + ids_modules_allowed = [module["id_module"] for module in modules if module["cruved"]["R"]] + + query = ( + select(TMonitoringModules) + .options( + Load(TMonitoringModules).raiseload("*"), + joinedload(TMonitoringModules.types_site).options(joinedload(BibTypeSite.sites)), + ) + .where( + and_( + TMonitoringModules.id_module.in_(ids_modules_allowed), + TMonitoringModules.types_site.any( + BibTypeSite.sites.any(id_base_site=id_base_site) + ), + ) + ) + ) + + schema = ModuleSchema() + result = db.session.scalars(query).unique().all() + # TODO: Is it usefull to put a limit here? Will there be more than 200 modules? + # If limit here, implement paginated/infinite scroll on frontend side + return [schema.dump(res) for res in result] + + +# TODO: vérfier si c'est utilisé +@blueprint.route("/sites/module/", methods=["GET"]) +def get_module_sites(module_code: str): + # TODO: load with site_categories.json API + return jsonify({"module_code": module_code}) + + +@blueprint.route("/sites", methods=["POST"], defaults={"object_type": "site"}) +@check_cruved_scope("C", module_code=MODULE_CODE, object_code="MONITORINGS_SITES") +def post_sites(object_type): + module_code = "generic" + object_type = "site" + post_data = dict(request.get_json()) + + # get_config(module_code, force=True) + + return create_or_update_object_api(module_code, object_type), 201 + + +@blueprint.route("/sites/", methods=["DELETE"], defaults={"object_type": "site"}) +@permissions.check_cruved_scope( + "D", get_scope=True, module_code=MODULE_CODE, object_code="MONITORINGS_SITES" +) +def delete_site(scope, _id, object_type): + site = db.get_or_404(TMonitoringSites, _id) + if not site.has_instance_permission(scope=scope): + raise Forbidden(f"User {g.current_user} cannot delete site {site.id_base_site}") + db.session.delete(site) + db.session.commit() + return {"success": "Item is successfully deleted"}, 200 + + +@blueprint.route("/sites/", methods=["PATCH"], defaults={"object_type": "site"}) +@permissions.check_cruved_scope( + "U", get_scope=True, module_code=MODULE_CODE, object_code="MONITORINGS_SITES" +) +def patch_sites(scope, _id, object_type): + site = db.get_or_404(TMonitoringSites, _id) + if not site.has_instance_permission(scope=scope): + raise Forbidden(f"User {g.current_user} cannot update site {site.id_base_site}") + module_code = "generic" + post_data = dict(request.get_json()) + + # get_config(module_code, force=True) + + return create_or_update_object_api(module_code, object_type, _id), 201 diff --git a/backend/gn_module_monitoring/routes/sites_groups.py b/backend/gn_module_monitoring/routes/sites_groups.py new file mode 100644 index 000000000..d9dbe9cc9 --- /dev/null +++ b/backend/gn_module_monitoring/routes/sites_groups.py @@ -0,0 +1,183 @@ +import json + +from flask import jsonify, request, g + +from marshmallow import ValidationError +from sqlalchemy import func, select +from werkzeug.datastructures import MultiDict +from werkzeug.exceptions import Forbidden + +from geonature.utils.env import db +from geonature.core.gn_permissions import decorators as permissions +from geonature.core.gn_permissions.decorators import check_cruved_scope + +from gn_module_monitoring import MODULE_CODE +from gn_module_monitoring.blueprint import blueprint +from gn_module_monitoring.config.repositories import get_config +from gn_module_monitoring.monitoring.models import TMonitoringSites, TMonitoringSitesGroups +from gn_module_monitoring.monitoring.schemas import MonitoringSitesGroupsDetailSchema +from gn_module_monitoring.utils.errors.errorHandler import InvalidUsage +from gn_module_monitoring.utils.routes import ( + filter_params, + geojson_query, + get_limit_page, + get_sort, + paginate_scope, + sort, + get_objet_with_permission_boolean, +) +from gn_module_monitoring.routes.monitoring import ( + create_or_update_object_api, + get_serialized_object, +) +from gn_module_monitoring.utils.utils import to_int + + +@blueprint.route("/sites_groups/config", methods=["GET"]) +def get_config_sites_groups(id=None, module_code="generic", object_type="sites_group"): + # A QUOI SERT CETTE ROUTE + obj = get_serialized_object(module_code, object_type, id) + return obj["properties"] + + +@blueprint.route("/sites_groups", methods=["GET"], defaults={"object_type": "sites_group"}) +@check_cruved_scope("R", module_code=MODULE_CODE, object_code="MONITORINGS_GRP_SITES") +def get_sites_groups(object_type: str): + object_code = "MONITORINGS_GRP_SITES" + params = MultiDict(request.args) + limit, page = get_limit_page(params=params) + + sort_label, sort_dir = get_sort( + params=params, default_sort="id_sites_group", default_direction="desc" + ) + query = select(TMonitoringSitesGroups) + query = filter_params(TMonitoringSitesGroups, query=query, params=params) + + query = sort(TMonitoringSitesGroups, query=query, sort=sort_label, sort_dir=sort_dir) + + query_allowed = TMonitoringSitesGroups.filter_by_readable(query=query, object_code=object_code) + return paginate_scope( + query=query_allowed, + schema=MonitoringSitesGroupsDetailSchema, + limit=limit, + page=page, + object_code=object_code, + ) + + +@blueprint.route( + "/sites_groups/", methods=["GET"], defaults={"object_type": "sites_group"} +) +@check_cruved_scope("R", module_code=MODULE_CODE, object_code="MONITORINGS_GRP_SITES") +@permissions.check_cruved_scope( + "R", get_scope=True, module_code=MODULE_CODE, object_code="MONITORINGS_GRP_SITES" +) +def get_sites_group_by_id(scope, id_sites_group: int, object_type: str): + sites_group = db.get_or_404(TMonitoringSitesGroups, id_sites_group) + if not sites_group.has_instance_permission(scope=scope): + raise Forbidden( + f"User {g.current_user} cannot read site group {sites_group.id_sites_group}" + ) + schema = MonitoringSitesGroupsDetailSchema() + response = schema.dump(sites_group) + response["cruved"] = get_objet_with_permission_boolean( + [sites_group], object_code="MONITORINGS_GRP_SITES" + )[0]["cruved"] + response["geometry"] = ( + json.loads(response["geometry"]) + if response["geometry"] != None and isinstance(response["geometry"], str) + else response["geometry"] + ) + return response + + +@blueprint.route( + "/sites_groups/geometries", methods=["GET"], defaults={"object_type": "sites_group"} +) +@check_cruved_scope("R", module_code=MODULE_CODE, object_code="MONITORINGS_GRP_SITES") +def get_sites_group_geometries(object_type: str): + + params = request.args.to_dict(flat=True) + object_code = "MONITORINGS_GRP_SITES" + query = select(TMonitoringSitesGroups) + query = TMonitoringSitesGroups.filter_by_readable(query=query, object_code=object_code) + query = TMonitoringSitesGroups.filter_by_params(query=query, params=params) + subquery_not_geom = ( + query.with_only_columns( + TMonitoringSitesGroups.id_sites_group, + TMonitoringSitesGroups.sites_group_name, + func.st_convexHull(func.st_collect(TMonitoringSites.geom)), + ) + .group_by(TMonitoringSitesGroups.id_sites_group, TMonitoringSitesGroups.sites_group_name) + .join( + TMonitoringSites, + TMonitoringSites.id_sites_group == TMonitoringSitesGroups.id_sites_group, + ) + .where(TMonitoringSitesGroups.geom == None) + ) + + subquery_with_geom = ( + query.with_only_columns( + TMonitoringSitesGroups.id_sites_group, + TMonitoringSitesGroups.sites_group_name, + TMonitoringSitesGroups.geom, + ).where(TMonitoringSitesGroups.geom != None) + ).distinct() + + results = geojson_query(subquery_not_geom.union(subquery_with_geom).alias("grp_site")) + + return jsonify(results) + + +@blueprint.route( + "/sites_groups/", methods=["PATCH"], defaults={"object_type": "sites_group"} +) +@permissions.check_cruved_scope( + "U", get_scope=True, module_code=MODULE_CODE, object_code="MONITORINGS_GRP_SITES" +) +def patch(scope, _id: int, object_type: str): + # ###############################"" + # FROM route/monitorings + sites_group = db.get_or_404(TMonitoringSitesGroups, _id) + if not sites_group.has_instance_permission(scope=scope): + raise Forbidden( + f"User {g.current_user} cannot update site group {sites_group.id_sites_group}" + ) + + module_code = "generic" + # get_config(module_code, force=True) + return create_or_update_object_api(module_code, object_type, _id), 201 + + +@blueprint.route( + "/sites_groups/", methods=["DELETE"], defaults={"object_type": "sites_group"} +) +@permissions.check_cruved_scope( + "D", get_scope=True, module_code=MODULE_CODE, object_code="MONITORINGS_GRP_SITES" +) +def delete(scope, _id: int, object_type: str): + sites_group = db.get_or_404(TMonitoringSitesGroups, _id) + if not sites_group.has_instance_permission(scope=scope): + raise Forbidden( + f"User {g.current_user} cannot delete site group {sites_group.id_sites_group}" + ) + db.session.delete(sites_group) + db.session.commit() + return {"success": "Item is successfully deleted"}, 200 + + +@blueprint.route("/sites_groups", methods=["POST"], defaults={"object_type": "sites_group"}) +@check_cruved_scope("C", module_code=MODULE_CODE, object_code="MONITORINGS_GRP_SITES") +def post(object_type: str): + module_code = "generic" + # get_config(module_code, force=True) + return create_or_update_object_api(module_code, object_type), 201 + + +@blueprint.errorhandler(ValidationError) +def handle_validation_error(error): + return InvalidUsage( + "Fields cannot be validated, message : {}".format(error.messages), + status_code=422, + payload=error.data, + ).to_dict() diff --git a/backend/gn_module_monitoring/routes/visit.py b/backend/gn_module_monitoring/routes/visit.py new file mode 100644 index 000000000..212d715c6 --- /dev/null +++ b/backend/gn_module_monitoring/routes/visit.py @@ -0,0 +1,54 @@ +from flask import request, current_app +from sqlalchemy import select +from sqlalchemy.orm import joinedload +from werkzeug.datastructures import MultiDict + +from geonature.utils.env import db + +from gn_module_monitoring.blueprint import blueprint +from gn_module_monitoring.monitoring.models import TMonitoringVisits +from gn_module_monitoring.monitoring.schemas import MonitoringVisitsSchema +from gn_module_monitoring.utils.routes import ( + filter_params, + get_limit_page, + get_sort, + paginate_scope, + sort, + get_objet_with_permission_boolean, +) +from gn_module_monitoring.routes.modules import get_modules + + +@blueprint.route("/visits", methods=["GET"], defaults={"object_type": "visit"}) +def get_visits(object_type): + params = MultiDict(request.args) + limit, page = get_limit_page(params=params) + sort_label, sort_dir = get_sort( + params=params, default_sort="id_base_visit", default_direction="desc" + ) + modules_object = get_modules() + + # Retrieves visits that do not depend on modules + OBJECT_CODE = current_app.config["MONITORINGS"].get("PERMISSION_LEVEL", {})["visit"] + + modules = get_objet_with_permission_boolean(modules_object, object_code=OBJECT_CODE) + ids_modules_allowed = [module["id_module"] for module in modules if module["cruved"]["R"]] + query = select(TMonitoringVisits) + query = query.options(joinedload(TMonitoringVisits.module)).where( + TMonitoringVisits.id_module.in_(ids_modules_allowed) + ) + query = filter_params(TMonitoringVisits, query=query, params=params) + query = sort(model=TMonitoringVisits, query=query, sort=sort_label, sort_dir=sort_dir) + query_allowed = query + for module in modules: + if module["id_module"] in ids_modules_allowed: + query_allowed = TMonitoringVisits.filter_by_readable( + query=query_allowed, module_code=module["module_code"], object_code=OBJECT_CODE + ) + return paginate_scope( + query=query_allowed, + schema=MonitoringVisitsSchema, + limit=limit, + page=page, + object_code=OBJECT_CODE, + ) diff --git a/backend/gn_module_monitoring/tests/__init__.py b/backend/gn_module_monitoring/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/fixtures/TestData/config_type_site.json b/backend/gn_module_monitoring/tests/fixtures/TestData/config_type_site.json new file mode 100644 index 000000000..05b07e4c5 --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/TestData/config_type_site.json @@ -0,0 +1,106 @@ +{ + "cruved": { + "C": 1, + "U": 1, + "D": 3 + }, + "sorts": [ + { + "prop": "base_site_name", + "dir": "asc" + } + ], + "filters": {}, + "label": "Gite", + "label_list": "Gites", + "genre": "M", + "geometry_type": "Point", + "display_properties": [ + "base_site_name", + "base_site_code", + "roost_type", + "nb_visits", + "threat", + "recommandation", + "opening", + "owner_name", + "owner_adress", + "owner_tel", + "owner_mail", + "medias" + ], + "display_list": [ + "base_site_name", + "roost_type", + "nb_visits", + "owner_name" + ], + "specific": { + "roost_type": { + "type_widget": "select", + "required": true, + "attribut_label": "Type de gite", + "values": [ + "barrage", + "bâtiment", + "cave", + "Ebouli", + "église", + "façade", + "four", + "garage", + "Gîte artificiel", + "grange", + "grenier", + "grotte", + "maison", + "mine", + "mur", + "panneau", + "plancher", + "pont", + "ruine", + "toit", + "toit et volet", + "transformateur", + "tunnel", + "volet", + "Autre" + ] + }, + "place_name": { + "type_widget": "text", + "attribut_label": "Lieux-dit" + }, + "owner_name": { + "type_widget": "text", + "attribut_label": "Nom propriétaire" + }, + "owner_adress": { + "type_widget": "text", + "attribut_label": "Adresse propriétaire" + }, + "owner_tel": { + "type_widget": "text", + "attribut_label": "Tel propriétaire" + }, + "owner_mail": { + "type_widget": "text", + "attribut_label": "Email propriétaire" + }, + "opening": { + "type_widget": "textarea", + "attribut_label": "Ouverture" + }, + "threat": { + "type_widget": "textarea", + "attribut_label": "Menace(s)", + "rows": 3 + }, + "recommandation": { + "type_widget": "textarea", + "attribut_label": "Mesure(s) préconisé(s)", + "rows": 3 + } + } + } \ No newline at end of file diff --git a/backend/gn_module_monitoring/tests/fixtures/TestData/config_type_site_type_utils.json b/backend/gn_module_monitoring/tests/fixtures/TestData/config_type_site_type_utils.json new file mode 100644 index 000000000..36f550d80 --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/TestData/config_type_site_type_utils.json @@ -0,0 +1,87 @@ +{ + + "filters": {}, + "label": "TEST", + "label_list": "TESTS", + "genre": "M", + "geometry_type": "Point", + "display_properties": [ + "base_site_name", + "base_site_code", + "id_nomenclature_sex", + "place_name", + "observers3", + "cd_nom_test" + ], + "display_list": [ + "base_site_name", + "base_site_code", + "id_nomenclature_sex", + "place_name", + "observers3", + "cd_nom_test" + ], + "specific": { + "id_nomenclature_sex": { + "type_widget": "nomenclature", + "attribut_label": "Sexe", + "code_nomenclature_type": "SEXE", + "type_util": "nomenclature", + "required": true, + "value": { + "code_nomenclature_type": "SEXE", + "cd_nomenclature": "6" + }, + "cd_nomenclatures": ["0", "2", "3", "6"] + }, + "place_name": { + "type_widget": "text", + "attribut_label": "Lieux-dit" + }, + "observers3": { + "attribut_label": "Observateurs", + "code_list": "1000005", + "hidden": false, + "required": true, + "type_util": "user", + "type_widget": "observers" + }, + "cd_nom_test": { + "attribut_label": "Taxon", + "id_list": "1000001", + "required": true, + "type_util": "taxonomy", + "type_widget": "taxonomy" + }, + "multi_id_nomenclature_sex": { + "type_widget": "nomenclature", + "attribut_label": "Sexe", + "code_nomenclature_type": "SEXE", + "type_util": "nomenclature", + "required": true, + "value": { + "code_nomenclature_type": "SEXE", + "cd_nomenclature": "6" + }, + "cd_nomenclatures": ["0", "2", "3", "6"], + "multiple" : true + }, + "multi_observers3": { + "attribut_label": "Observateurs", + "code_list": "1000005", + "hidden": false, + "required": true, + "type_util": "user", + "type_widget": "observers", + "multiple" : true + }, + "multi_cd_nom_test": { + "attribut_label": "Taxon", + "id_list": "1000001", + "required": true, + "type_util": "taxonomy", + "type_widget": "taxonomy", + "multiple" : true + } + } + } \ No newline at end of file diff --git a/backend/gn_module_monitoring/tests/fixtures/__init__.py b/backend/gn_module_monitoring/tests/fixtures/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/fixtures/bad_sql_error.sql b/backend/gn_module_monitoring/tests/fixtures/bad_sql_error.sql new file mode 100644 index 000000000..e1b736b88 --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/bad_sql_error.sql @@ -0,0 +1,2 @@ +SELECT * +FROM ma_table_qui_n_existe_pas; \ No newline at end of file diff --git a/backend/gn_module_monitoring/tests/fixtures/bad_sql_forbidden.sql b/backend/gn_module_monitoring/tests/fixtures/bad_sql_forbidden.sql new file mode 100644 index 000000000..2ac3184ba --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/bad_sql_forbidden.sql @@ -0,0 +1,2 @@ +DELETE ; +TRUNCATE ; \ No newline at end of file diff --git a/backend/gn_module_monitoring/tests/fixtures/generic.py b/backend/gn_module_monitoring/tests/fixtures/generic.py new file mode 100644 index 000000000..f5fa26bd2 --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/generic.py @@ -0,0 +1,102 @@ +import pytest + +from sqlalchemy import select + +from geonature.utils.env import db +from geonature.core.gn_permissions.models import ( + PermAction, + PermObject, + Permission, +) +from geonature.core.gn_commons.models import TModules + +from pypnusershub.db.models import User +from pypnusershub.db.models import ( + User, + Organisme, + Application, + Profils as Profil, + UserApplicationRight, +) + + +@pytest.fixture(scope="session") +def monitorings_users(app): + app = db.session.execute( + select(Application).where(Application.code_application == "GN") + ).scalar_one() + profil = db.session.execute(select(Profil).where(Profil.nom_profil == "Lecteur")).scalar_one() + + modules = db.session.scalars(select(TModules)).all() + + actions = { + code: db.session.execute( + select(PermAction).where(PermAction.code_action == code) + ).scalar_one() + for code in "CRUVED" + } + type_code_object = [ + "MONITORINGS_MODULES", + "MONITORINGS_GRP_SITES", + "MONITORINGS_SITES", + "MONITORINGS_VISITES", + "ALL", + ] + + def create_user(username, organisme=None, scope=None, sensitivity_filter=False): + # do not commit directly on current transaction, as we want to rollback all changes at the end of tests + with db.session.begin_nested(): + user = User( + groupe=False, + active=True, + organisme=organisme, + identifiant=username, + password=username, + nom_role=username, + prenom_role=username, + ) + db.session.add(user) + # user must have been commited for user.id_role to be defined + with db.session.begin_nested(): + # login right + right = UserApplicationRight( + id_role=user.id_role, id_application=app.id_application, id_profil=profil.id_profil + ) + db.session.add(right) + if scope > 0: + for co in type_code_object: + object_all = db.session.scalars( + select(PermObject).where(PermObject.code_object == co) + ).all() + for action in actions.values(): + for module in modules: + for obj in object_all + module.objects: + permission = Permission( + role=user, + action=action, + module=module, + object=obj, + scope_value=scope if scope != 3 else None, + sensitivity_filter=sensitivity_filter, + ) + db.session.add(permission) + return user + + users = {} + + organisme = Organisme(nom_organisme="Autre") + db.session.add(organisme) + + users_to_create = [ + ("noright_user", organisme, 0), + ("stranger_user", None, 2), + ("associate_user", organisme, 2), + ("self_user", organisme, 1), + ("user", organisme, 2), + ("admin_user", organisme, 3), + ] + + for username, *args in users_to_create: + users[username] = create_user(username, *args) + + return users diff --git a/backend/gn_module_monitoring/tests/fixtures/module.py b/backend/gn_module_monitoring/tests/fixtures/module.py new file mode 100644 index 000000000..fb2504ebd --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/module.py @@ -0,0 +1,113 @@ +import pytest +import pytest +import shutil + +from uuid import uuid4 +from pathlib import Path +from flask import current_app + +from sqlalchemy import select + +from geonature.utils.env import db, BACKEND_DIR +from geonature.core.gn_permissions.models import ( + PermAction, + PermObject, + Permission, +) + +from gn_module_monitoring.monitoring.models import TMonitoringModules +from gn_module_monitoring.command.cmd import ( + cmd_install_monitoring_module, +) +from gn_module_monitoring.monitoring.models import TMonitoringModules +from gn_module_monitoring.tests.fixtures.generic import monitorings_users +from gn_module_monitoring.tests.fixtures.type_site import types_site + + +@pytest.fixture +def install_module_test(types_site): + # Copy des fichiers du module de test + path_gn_monitoring = Path(__file__).absolute().parent.parent.parent.parent.parent + path_module_test = path_gn_monitoring / Path("contrib/test") + path_gn_monitoring = BACKEND_DIR / Path("media/monitorings/test") + shutil.copytree(src=str(path_module_test), dst=str(path_gn_monitoring), dirs_exist_ok=True) + + # Installation du module + runner = current_app.test_cli_runner() + result = runner.invoke(cmd_install_monitoring_module, ["test"]) + + assert result.exit_code == 0 + # Association du module aux types de site existant + module = db.session.execute( + select(TMonitoringModules).where(TMonitoringModules.module_code == "test") + ).scalar_one() + with db.session.begin_nested(): + module.types_site = list(types_site.values()) + db.session.add(module) + + +@pytest.fixture +def monitoring_module(types_site, monitorings_users): + t_monitoring_module = TMonitoringModules( + module_code="TEST", + uuid_module_complement=uuid4(), + module_label="test", + active_frontend=True, + active_backend=False, + b_synthese=False, + module_path="test", + types_site=list(types_site.values()), + ) + + with db.session.begin_nested(): + db.session.add(t_monitoring_module) + # Set module Permission + + actions = { + code: db.session.execute( + select(PermAction).where(PermAction.code_action == code) + ).scalar_one() + for code in "CRUVED" + } + + type_code_object = [ + "MONITORINGS_MODULES", + "MONITORINGS_GRP_SITES", + "MONITORINGS_SITES", + "MONITORINGS_VISITES", + ] + for co in type_code_object: + object_all = db.session.execute( + select(PermObject).where(PermObject.code_object == co) + ).scalar_one() + + for action in actions.values(): + for obj in [object_all] + t_monitoring_module.objects: + permission = Permission( + role=monitorings_users["admin_user"], + action=action, + module=t_monitoring_module, + object=obj, + scope_value=None, + sensitivity_filter=None, + ) + db.session.add(permission) + + return t_monitoring_module + + +@pytest.fixture +def monitoring_module_wo_types_site(): + t_monitoring_module = TMonitoringModules( + module_code=uuid4(), + module_label="NoType", + active_frontend=True, + active_backend=False, + module_path="NoType", + b_synthese=False, + ) + + with db.session.begin_nested(): + db.session.add(t_monitoring_module) + + return t_monitoring_module diff --git a/backend/gn_module_monitoring/tests/fixtures/site.py b/backend/gn_module_monitoring/tests/fixtures/site.py new file mode 100644 index 000000000..cbc57b5f8 --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/site.py @@ -0,0 +1,133 @@ +import pytest +import json + +from sqlalchemy import select + +from geoalchemy2.shape import from_shape +from shapely.geometry import Point + +from geonature.utils.env import db + +from pypnnomenclature.models import TNomenclatures, BibNomenclaturesTypes +from gn_module_monitoring.monitoring.models import TMonitoringSites +from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema + + +@pytest.fixture() +def sites(monitorings_users, types_site, site_group_with_sites): + user = monitorings_users["user"] + geom_4326 = from_shape(Point(43, 24), srid=4326) + sites = {} + for i, key in enumerate(types_site.keys()): + sites[key] = TMonitoringSites( + id_inventor=user.id_role, + id_digitiser=user.id_role, + base_site_name=f"Site{i}", + base_site_description=f"Description{i}", + base_site_code=f"Code{i}", + geom=geom_4326, + types_site=[types_site[key]], + id_sites_group=site_group_with_sites.id_sites_group, + ) + + # Add a special site that has no type + sites["no-type"] = TMonitoringSites( + id_inventor=user.id_role, + id_digitiser=user.id_role, + base_site_name="no-type", + base_site_description="Description-no-type", + base_site_code="Code-no-type", + geom=geom_4326, + types_site=[], + id_sites_group=site_group_with_sites.id_sites_group, + ) + + with db.session.begin_nested(): + db.session.add_all(sites.values()) + return sites + + +@pytest.fixture() +def sites_with_data_typeutils(users, types_site_type_utils, site_group_with_sites): + user = users["user"] + geom_4326 = from_shape(Point(43, 24), srid=4326) + sites = {} + nomenclature_sex = db.session.scalars( + select(TNomenclatures) + .where(TNomenclatures.nomenclature_type.has(BibNomenclaturesTypes.mnemonique == "SEXE")) + .where(TNomenclatures.cd_nomenclature == "2") + .limit(1) + ).first() + for i, key in enumerate(types_site_type_utils.keys()): + sites[key] = TMonitoringSites( + id_inventor=user.id_role, + id_digitiser=user.id_role, + base_site_name=f"Site{i}", + base_site_description=f"Description{i}", + base_site_code=f"Code{i}", + geom=geom_4326, + types_site=[types_site_type_utils[key]], + id_sites_group=site_group_with_sites.id_sites_group, + data={ + "observers3": user.id_role, + "cd_nom_test": 212, + "id_nomenclature_sex": nomenclature_sex.id_nomenclature, + "multiple_observers3": [user.id_role], + "multiple_cd_nom_test": [212, 99165], + "multiple_id_nomenclature_sex": [nomenclature_sex.id_nomenclature], + }, + ) + + with db.session.begin_nested(): + db.session.add_all(sites.values()) + return sites + + +@pytest.fixture() +def site_to_post_with_types(users, types_site, site_group_without_sites): + user = users["user"] + geom_4326 = from_shape(Point(43, 24), srid=4326) + list_nomenclature_id = [] + specific_dic = {"owner_name": "Propriétaire", "threat": "Menaces", "owner_tel": "0609090909"} + schema_type_site = BibTypeSiteSchema() + mock_db_type_site = [schema_type_site.dump(type) for type in types_site.values()] + + for type in mock_db_type_site: + list_nomenclature_id.append(type["id_nomenclature_type_site"]) + + site_to_post_with_types = TMonitoringSites( + id_inventor=user.id_role, + id_digitiser=user.id_role, + base_site_name="New Site", + base_site_description="New Description", + base_site_code="New Code", + geom=geom_4326, + # types_site=list_nomenclature_id, + id_sites_group=site_group_without_sites.id_sites_group, + ) + + post_data = dict() + post_data["dataComplement"] = {} + for type_site_dic in mock_db_type_site: + copy_dic = type_site_dic.copy() + copy_dic.pop("label") + post_data["dataComplement"][type_site_dic["label"]] = copy_dic + + post_data["dataComplement"]["types_site"] = list_nomenclature_id + + post_data["properties"] = MonitoringSitesSchema().dump(site_to_post_with_types) + + post_data["geometry"] = json.loads(post_data["properties"].pop("geometry")) + + post_data["type"] = "Feature" + post_data["properties"]["types_site"] = list_nomenclature_id + + for type_site in mock_db_type_site: + specific_config = type_site["config"]["specific"] + for key_specific in specific_config: + if key_specific in specific_dic.keys(): + post_data["properties"][key_specific] = specific_dic[key_specific] + else: + post_data["properties"][key_specific] = None + + return post_data diff --git a/backend/gn_module_monitoring/tests/fixtures/sites_groups.py b/backend/gn_module_monitoring/tests/fixtures/sites_groups.py new file mode 100644 index 000000000..5cb8d9bda --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/sites_groups.py @@ -0,0 +1,27 @@ +import pytest + +from geonature.utils.env import db + +from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups + + +@pytest.fixture +def sites_groups(): + names = ["Site_eolien", "Site_Groupe"] + + groups = {name: TMonitoringSitesGroups(sites_group_name=name) for name in names} + + with db.session.begin_nested(): + db.session.add_all(groups.values()) + + return groups + + +@pytest.fixture +def site_group_with_sites(sites_groups): + return sites_groups["Site_Groupe"] + + +@pytest.fixture +def site_group_without_sites(sites_groups): + return sites_groups["Site_eolien"] diff --git a/backend/gn_module_monitoring/tests/fixtures/type_site.py b/backend/gn_module_monitoring/tests/fixtures/type_site.py new file mode 100644 index 000000000..e5a614b06 --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/type_site.py @@ -0,0 +1,73 @@ +import pytest +import json +import os + +from sqlalchemy import select + +from geonature.utils.env import db + +from pypnnomenclature.models import BibNomenclaturesTypes, TNomenclatures + +from geonature.core.gn_monitoring.models import BibTypeSite + + +def get_test_data(filename): + folder_path = os.path.abspath(os.path.dirname(__file__)) + folder = os.path.join(folder_path, "TestData") + jsonfile = os.path.join(folder, filename) + with open(jsonfile) as file: + data = json.load(file) + return data + + +@pytest.fixture +def nomenclature_types_site(): + mnemoniques = ("Test_Grotte", "Test_Mine") + nomenclatures = [] + type_site = db.session.scalars( + select(BibNomenclaturesTypes) + .where(BibNomenclaturesTypes.mnemonique == "TYPE_SITE") + .limit(1) + ).first() + for mnemo in mnemoniques: + nomenclatures.append( + TNomenclatures( + id_type=type_site.id_type, + cd_nomenclature=mnemo, + label_default=mnemo, + label_fr=mnemo, + active=True, + ) + ) + with db.session.begin_nested(): + db.session.add_all(nomenclatures) + return nomenclatures + + +@pytest.fixture +def types_site(nomenclature_types_site): + config_type_site = get_test_data("config_type_site.json") + types_site = { + nomenc_type_site.label_default: BibTypeSite( + id_nomenclature_type_site=nomenc_type_site.id_nomenclature, config=config_type_site + ) + for nomenc_type_site in nomenclature_types_site + } + with db.session.begin_nested(): + db.session.add_all(types_site.values()) + return types_site + + +@pytest.fixture +def types_site_type_utils(nomenclature_types_site): + + config_type_site = get_test_data("config_type_site_type_utils.json") + types_site = { + nomenc_type_site.label_default: BibTypeSite( + id_nomenclature_type_site=nomenc_type_site.id_nomenclature, config=config_type_site + ) + for nomenc_type_site in nomenclature_types_site + } + with db.session.begin_nested(): + db.session.add_all(types_site.values()) + return types_site diff --git a/backend/gn_module_monitoring/tests/fixtures/visit.py b/backend/gn_module_monitoring/tests/fixtures/visit.py new file mode 100644 index 000000000..4ea007d41 --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/visit.py @@ -0,0 +1,26 @@ +import datetime +import pytest + +from geonature.utils.env import db + +from gn_module_monitoring.monitoring.models import TMonitoringVisits + + +@pytest.fixture +def visits(sites, datasets, monitoring_module): + now = datetime.datetime.now() + dataset = datasets["orphan_dataset"] + db_visits = [] + for site in sites.values(): + db_visits.append( + TMonitoringVisits( + id_base_site=site.id_base_site, + id_module=monitoring_module.id_module, + id_dataset=dataset.id_dataset, + visit_date_min=now, + ) + ) + with db.session.begin_nested(): + db.session.add_all(db_visits) + + return db_visits diff --git a/backend/gn_module_monitoring/tests/test_commands/__init__.py b/backend/gn_module_monitoring/tests/test_commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/test_commands/test_commands.py b/backend/gn_module_monitoring/tests/test_commands/test_commands.py new file mode 100644 index 000000000..622d6c30b --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_commands/test_commands.py @@ -0,0 +1,103 @@ +import pytest + +from flask import url_for, current_app + +from sqlalchemy import select + +from geonature.utils.env import DB + +from gn_module_monitoring.tests.fixtures.generic import * +from gn_module_monitoring.command.cmd import ( + cmd_remove_monitoring_module_cmd, + cmd_process_sql, + cmd_process_available_permission_module, + cmd_add_module_nomenclature_cli, +) +from gn_module_monitoring.monitoring.models import TMonitoringModules + + +@pytest.mark.usefixtures("client_class", "temporary_transaction") +class TestCommands: + def test_install_monitoring_module(self, install_module_test): + # Installation du module + # Test Installation + result = DB.session.execute( + select(TMonitoringModules).where(TMonitoringModules.module_code == "test") + ).scalar_one() + assert result.module_code == "test" + + def test_remove_monitoring_module(self, install_module_test): + runner = current_app.test_cli_runner() + + # Suppression du module de test + result = runner.invoke(cmd_remove_monitoring_module_cmd, ["test"]) + + # Test suppression + result = DB.session.execute( + select(TMonitoringModules).where(TMonitoringModules.module_code == "test") + ).scalar_one_or_none() + assert result == None + + def test_process_all_with_module(self, install_module_test): + runner = current_app.test_cli_runner() + # Commande process all + result = runner.invoke(cmd_process_sql, ["test"]) + # Pas de result juste + assert result.exit_code == 0 + + def test_process_all_without_module(self, install_module_test): + runner = current_app.test_cli_runner() + # Commande process all + result = runner.invoke(cmd_process_sql) + # Pas de result + assert result.exit_code == 0 + + def test_process_all_with_module(self, install_module_test): + runner = current_app.test_cli_runner() + # Commande process all + # import pdb + result = runner.invoke(cmd_process_sql, ["test"]) + # Pas de result juste + assert result.exit_code == 0 + + def test_process_available_permission_module_without_module(self, install_module_test): + runner = current_app.test_cli_runner() + # Commande process all + result = runner.invoke(cmd_process_available_permission_module) + # Pas de result juste + assert result.exit_code == 0 + assert "Création des permissions pour test" in result.output + + def test_process_available_permission_module_with_module(self, install_module_test): + runner = current_app.test_cli_runner() + # Commande process all + result = runner.invoke(cmd_process_available_permission_module, ["test"]) + # Pas de result juste + assert result.exit_code == 0 + assert "Création des permissions pour test" in result.output + + def test_process_available_permission_module_bad_module(self, install_module_test): + runner = current_app.test_cli_runner() + # Commande process all + result = runner.invoke(cmd_process_available_permission_module, ["bad_module"]) + # Pas de result juste + assert result.exit_code == 0 + assert "le module n'existe pas" in result.output + + def test_cmd_add_module_nomenclature_cli(self, install_module_test): + runner = current_app.test_cli_runner() + # Commande process all + result = runner.invoke(cmd_add_module_nomenclature_cli) + # Pas de result juste + assert result.exit_code == 2 + assert "Missing argument 'MODULE_CODE'" in result.output + + def test_cmd_add_module_nomenclature_cli(self, install_module_test): + runner = current_app.test_cli_runner() + # Commande add_module_nomenclature + result = runner.invoke(cmd_add_module_nomenclature_cli, ["test"]) + # Pas de result juste + assert result.exit_code == 0 + assert "nomenclature type TEST_METEO - Météo - already exist" in result.output + assert "nomenclature METEO_M - Mauvais temps - updated" in result.output + assert 'probleme de type avec mnemonique="TEST_UNKWONW_TYPE"' in result.output diff --git a/backend/gn_module_monitoring/tests/test_commands/test_utils.py b/backend/gn_module_monitoring/tests/test_commands/test_utils.py new file mode 100644 index 000000000..f1a31ad34 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_commands/test_utils.py @@ -0,0 +1,33 @@ +import pytest + +from pathlib import Path +from flask import url_for, current_app + +from sqlalchemy import select + +from geonature.utils.env import DB + +from gn_module_monitoring.tests.fixtures.generic import * +from gn_module_monitoring.command.utils import execute_sql_file, FORBIDDEN_SQL_INSTRUCTION +from gn_module_monitoring.monitoring.models import TMonitoringModules + + +@pytest.mark.usefixtures("client_class", "temporary_transaction") +class TestCommandsUtils: + def test_execute_sql_file(self): + file_dir = Path(__file__).absolute().parent.parent / "fixtures" + file_name = "bad_sql_forbidden.sql" + + with pytest.raises(Exception) as excinfo: + execute_sql_file(file_dir, file_name, "module_code", FORBIDDEN_SQL_INSTRUCTION) + + assert ( + str(excinfo.value) + == "erreur dans le script module_code instruction sql non autorisée bad_sql_forbidden.sql" + ) + + file_name = "bad_sql_error.sql" + with pytest.raises(Exception) as excinfo: + execute_sql_file(file_dir, file_name, "module_code", FORBIDDEN_SQL_INSTRUCTION) + + assert "erreur dans le script bad_sql_error.sql" in str(excinfo.value) diff --git a/backend/gn_module_monitoring/tests/test_monitoring/__init__.py b/backend/gn_module_monitoring/tests/test_monitoring/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_models/__init__.py b/backend/gn_module_monitoring/tests/test_monitoring/test_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_bib_type_site.py b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_bib_type_site.py new file mode 100644 index 000000000..35b01791f --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_bib_type_site.py @@ -0,0 +1,28 @@ +import pytest + +from sqlalchemy import select + +from geonature.utils.env import db +from geonature.core.gn_monitoring.models import BibTypeSite + + +@pytest.mark.usefixtures("temporary_transaction") +class TestBibTypeSite: + def test_get_bib_type_site(self, types_site): + type_site = list(types_site.values())[0] + get_type_site = db.session.execute( + select(BibTypeSite).where( + BibTypeSite.id_nomenclature_type_site == type_site.id_nomenclature_type_site + ) + ).scalar_one() + + assert get_type_site.id_nomenclature_type_site == type_site.id_nomenclature_type_site + + def test_get_all_bib_type_site(self, types_site): + get_types_site = db.session.scalars(select(BibTypeSite)).all() + + assert all( + type_site.id_nomenclature_type_site + in [get_type_site.id_nomenclature_type_site for get_type_site in get_types_site] + for type_site in types_site.values() + ) diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_module.py b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_module.py new file mode 100644 index 000000000..2e66e56ff --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_module.py @@ -0,0 +1,26 @@ +import pytest + +from sqlalchemy import select + +from geonature.utils.env import db + +from gn_module_monitoring.monitoring.models import TMonitoringModules + + +@pytest.mark.usefixtures("temporary_transaction") +class TestModule: + def test_module(self, monitoring_module, types_site): + types = monitoring_module.types_site + assert types == list(types_site.values()) + + def test_remove_categorie_from_module(self, monitoring_module, types_site): + with db.session.begin_nested(): + monitoring_module.types_site.pop(0) + + module = db.session.execute( + select(TMonitoringModules).where( + TMonitoringModules.id_module == monitoring_module.id_module + ) + ).scalar_one() + + assert len(module.types_site) == len(types_site) - 1 diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_sites_groups.py b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_sites_groups.py new file mode 100644 index 000000000..85121ad9c --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_sites_groups.py @@ -0,0 +1,48 @@ +import pytest + +from sqlalchemy import select + +from geonature.utils.env import DB + +from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups + + +@pytest.mark.usefixtures("temporary_transaction") +class TestTMonitoringSitesGroups: + def test_sort_desc(self, sites_groups): + if len(sites_groups) < 2: + pytest.xfail( + "This test cannot work if there is less than 2 sites_groups in database (via fixtures or not)" + ) + + query = ( + select(TMonitoringSitesGroups) + .where( + TMonitoringSitesGroups.id_sites_group.in_( + group.id_sites_group for group in sites_groups.values() + ) + ) + .order_by(TMonitoringSitesGroups.id_sites_group.desc()) + ) + result = DB.session.scalars(query).all() + + assert result[0].id_sites_group > result[1].id_sites_group + + def test_sort_asc(self, sites_groups): + if len(sites_groups) < 2: + pytest.xfail( + "This test cannot work if there is less than 2 sites_groups in database (via fixtures or not)" + ) + + query = ( + select(TMonitoringSitesGroups) + .where( + TMonitoringSitesGroups.id_sites_group.in_( + group.id_sites_group for group in sites_groups.values() + ) + ) + .order_by(TMonitoringSitesGroups.id_sites_group.asc()) + ) + result = DB.session.scalars(query).all() + + assert result[0].id_sites_group < result[1].id_sites_group diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_schemas/test_bib_site_type_schema.py b/backend/gn_module_monitoring/tests/test_monitoring/test_schemas/test_bib_site_type_schema.py new file mode 100644 index 000000000..6d55382af --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_schemas/test_bib_site_type_schema.py @@ -0,0 +1,18 @@ +import pytest + +from sqlalchemy import select + +from geonature.utils.env import db + +from geonature.core.gn_monitoring.models import BibTypeSite +from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema + + +@pytest.mark.usefixtures("temporary_transaction") +class TestBibSiteTypeSchema: + def test_dump(self, types_site): + one_type_site = db.session.scalars(select(BibTypeSite).limit(1)).first() + schema = BibTypeSiteSchema() + type_site = schema.dump(one_type_site) + + assert type_site["id_nomenclature_type_site"] == one_type_site.id_nomenclature_type_site diff --git a/backend/gn_module_monitoring/tests/test_routes/__init__.py b/backend/gn_module_monitoring/tests/test_routes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/test_routes/test_config.py b/backend/gn_module_monitoring/tests/test_routes/test_config.py new file mode 100644 index 000000000..3cfdc1511 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_routes/test_config.py @@ -0,0 +1,26 @@ +import pytest +from flask import url_for + +from gn_module_monitoring.tests.fixtures.generic import * +from pypnusershub.tests.utils import set_logged_user_cookie + + +@pytest.mark.usefixtures("client_class", "temporary_transaction") +class TestRouteConfig: + def test_get_config(self): + response = self.client.get(url_for("monitorings.get_config_api")) + + assert response.json["default_display_field_names"]["area"] == "area_name" + + def test_get_config_module(self, install_module_test, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + response = self.client.get(url_for("monitorings.get_config_api", module_code="test")) + + module_type_site = response.json["module"]["types_site"] + type_site_name = [v["name"] for k, v in module_type_site.items()] + + assert set(type_site_name) == set(["Test_Grotte", "Test_Mine"]) + for id, type_site in module_type_site.items(): + assert set(type_site["display_properties"]).issubset( + [k for k in response.json["site"]["specific"]] + ) diff --git a/backend/gn_module_monitoring/tests/test_routes/test_data_utils.py b/backend/gn_module_monitoring/tests/test_routes/test_data_utils.py new file mode 100644 index 000000000..e042dfc63 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_routes/test_data_utils.py @@ -0,0 +1,85 @@ +import pytest +from flask import url_for + + +@pytest.mark.usefixtures("client_class", "temporary_transaction") +class TestRouteDataUtils: + def test_get_init_data(self, install_module_test): + response = self.client.get( + url_for("monitorings.get_init_data", module_code="test"), + ) + assert response.status_code == 200 + assert response.json["nomenclature"][0]["cd_nomenclature"] == "2" + + def test_get_util_nomenclature_api_valid(self, install_module_test): + code_nomenclature_type = "TYPE_MEDIA" + cd_nomenclature = "2" + + valid_response = self.client.get( + url_for( + "monitorings.get_util_nomenclature_api", + code_nomenclature_type=code_nomenclature_type, + cd_nomenclature=cd_nomenclature, + ), + ) + assert valid_response.status_code == 200 + + def test_get_util_nomenclature_api_invalid(self, install_module_test): + code_nomenclature_type = "TYPE_MEDIAsss" + cd_nomenclature = "2" + + error_response = self.client.get( + url_for( + "monitorings.get_util_nomenclature_api", + code_nomenclature_type=code_nomenclature_type, + cd_nomenclature=cd_nomenclature, + ), + ) + assert error_response.status_code == 500 + + def test_get_util_from_id_api_valid(self, install_module_test): + nomenclature_response = self.client.get( + url_for( + "monitorings.get_util_from_id_api", + type_util="nomenclature", + id="2", + ), + ) + assert nomenclature_response.status_code == 200 + + user_response = self.client.get( + url_for( + "monitorings.get_util_from_id_api", + type_util="user", + id="4", + ), + ) + assert user_response.status_code == 200 + + taxonomy_response = self.client.get( + url_for( + "monitorings.get_util_from_id_api", + type_util="taxonomy", + id="1000", + ), + ) + assert taxonomy_response.status_code == 200 + + def test_get_util_from_id_api_invalid(self, install_module_test): + invalid_type_response = self.client.get( + url_for( + "monitorings.get_util_from_id_api", + type_util="azefazefazefazefazefze", + id="1", + ), + ) + assert invalid_type_response.status_code == 204 + + invalid_id_response = self.client.get( + url_for( + "monitorings.get_util_from_id_api", + type_util="taxonomy", + id="1", + ), + ) + assert invalid_type_response.status_code == 204 diff --git a/backend/gn_module_monitoring/tests/test_routes/test_modules.py b/backend/gn_module_monitoring/tests/test_routes/test_modules.py new file mode 100644 index 000000000..a5aa62ecc --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_routes/test_modules.py @@ -0,0 +1,22 @@ +import pytest + +from flask import url_for + +from pypnusershub.tests.utils import set_logged_user_cookie + +from gn_module_monitoring.tests.fixtures.generic import * + + +@pytest.mark.usefixtures("client_class", "temporary_transaction") +class TestModules: + def test_get_modules_api(self, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + r = self.client.get(url_for("monitorings.get_modules_api")) + # TODO test response + assert r.status_code == 200 + + def test_get_cruved_monitorings(self, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + r = self.client.get(url_for("monitorings.get_cruved_monitorings")) + # TODO test response + assert r.status_code == 200 diff --git a/backend/gn_module_monitoring/tests/test_routes/test_monitoring.py b/backend/gn_module_monitoring/tests/test_routes/test_monitoring.py new file mode 100644 index 000000000..e536fb6dd --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_routes/test_monitoring.py @@ -0,0 +1,91 @@ +import pytest +from flask import url_for + +from geonature.utils.env import db +from geonature.core.gn_permissions.models import ( + PermAction, + PermObject, + Permission, +) + +from pypnusershub.tests.utils import set_logged_user_cookie + +from gn_module_monitoring.tests.fixtures.generic import * +from gn_module_monitoring.monitoring.models import TMonitoringModules + + +def add_user_permission(module_code, user, scope, type_code_object, code_action="CRUVED"): + module = db.session.execute( + select(TMonitoringModules).where(TMonitoringModules.module_code == module_code) + ).scalar_one() + actions = { + code: db.session.execute( + select(PermAction).where(PermAction.code_action == code) + ).scalar_one() + for code in code_action + } + with db.session.begin_nested(): + if scope > 0: + object_all = db.session.scalars( + select(PermObject).where(PermObject.code_object == type_code_object) + ).all() + for action in actions.values(): + for obj in object_all + module.objects: + permission = Permission( + role=user, + action=action, + module=module, + object=obj, + scope_value=scope if scope != 3 else None, + sensitivity_filter=None, + ) + db.session.add(permission) + + +@pytest.mark.usefixtures("client_class", "temporary_transaction") +class TestModules: + def test_get_fake_export_csv(self, install_module_test, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + # Add user permission for export + add_user_permission( + "test", + monitorings_users["admin_user"], + scope=3, + type_code_object="MONITORINGS_MODULES", + code_action="E", + ) + + # test unauthorized + response = self.client.get( + url_for("monitorings.export_all_observations", module_code="test", method="inexistant") + ) + assert response.status_code == 404 + + def test_get_export_csv(self, install_module_test, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + + # test unautorized + response = self.client.get( + url_for("monitorings.export_all_observations", module_code="test", method="sites") + ) + assert response.status_code == 403 + + # Add user permission for export + add_user_permission( + "test", + monitorings_users["admin_user"], + scope=3, + type_code_object="MONITORINGS_MODULES", + code_action="E", + ) + + response = self.client.get( + url_for("monitorings.export_all_observations", module_code="test", method="sites") + ) + + expected_headers_content_type = "text/plain" + expected = '"base_site_code";"longitude";"latitude"' + + assert response.status_code == 200 + assert response.headers.get("content-type") == expected_headers_content_type + assert expected in response.text diff --git a/backend/gn_module_monitoring/tests/test_routes/test_site.py b/backend/gn_module_monitoring/tests/test_routes/test_site.py new file mode 100644 index 000000000..0e3127a3c --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_routes/test_site.py @@ -0,0 +1,335 @@ +import pytest + +from flask import url_for + +from pypnusershub.tests.utils import set_logged_user_cookie + +from gn_module_monitoring.monitoring.models import TMonitoringSites +from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema +from gn_module_monitoring.monitoring.models import TMonitoringSites +from gn_module_monitoring.tests.fixtures.generic import * + + +@pytest.mark.usefixtures("client_class", "temporary_transaction") +class TestSite: + def test_get_type_site_by_id(self, types_site): + for type_site in types_site.values(): + r = self.client.get( + url_for( + "monitorings.get_type_site_by_id", + id_type_site=type_site.id_nomenclature_type_site, + ) + ) + assert r.json["id_nomenclature_type_site"] == type_site.id_nomenclature_type_site + + def test_get_types_site(self, types_site): + schema = BibTypeSiteSchema() + + r = self.client.get(url_for("monitorings.get_types_site")) + + assert r.json["count"] >= len(types_site) + assert all([schema.dump(cat) in r.json["items"] for cat in types_site.values()]) + + def test_get_sites(self, sites, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + schema = MonitoringSitesSchema() + + r = self.client.get(url_for("monitorings.get_sites")) + assert r.status_code == 200 + assert r.json["count"] >= len(sites) + + sites_response = r.json["items"] + for s in sites_response: + s.pop("cruved") + + assert any([schema.dump(site) in sites_response for site in sites.values()]) + + def test_get_sites_limit(self, sites, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + limit = 2 + + r = self.client.get(url_for("monitorings.get_sites", limit=limit)) + + assert len(r.json["items"]) == limit + + def test_get_sites_base_site_name(self, sites, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + site = list(sites.values())[0] + base_site_name = site.base_site_name + + r = self.client.get(url_for("monitorings.get_sites", base_site_name=base_site_name)) + + assert len(r.json["items"]) == 1 + assert r.json["items"][0]["base_site_name"] == base_site_name + + def test_get_sites_id_base_site(self, sites, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + site = list(sites.values())[0] + id_base_site = site.id_base_site + + r = self.client.get(url_for("monitorings.get_sites", id_base_site=id_base_site)) + + assert len(r.json["items"]) == 1 + assert r.json["items"][0]["id_base_site"] == id_base_site + + def test_get_sites_by_id(self, sites, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + site = list(sites.values())[0] + id_base_site = site.id_base_site + + r = self.client.get( + url_for("monitorings.get_site_by_id", id=id_base_site, object_type="site") + ) + + assert r.json["id_base_site"] == id_base_site + + def test_get_all_site_geometries(self, sites, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + r = self.client.get(url_for("monitorings.get_all_site_geometries")) + + json_resp = r.json + features = json_resp.get("features") + sites_values = list(sites.values()) + assert r.content_type == "application/json" + assert json_resp.get("type") == "FeatureCollection" + assert len(features) >= len(sites_values) + for site in sites_values: + id_ = [ + obj["properties"] + for obj in features + if obj["properties"]["base_site_name"] == site.base_site_name + ][0]["id_base_site"] + assert id_ == site.id_base_site + + def test_get_all_site_geometries_filter_site_group_without_sites( + self, sites, site_group_without_sites, monitorings_users + ): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + r = self.client.get( + url_for( + "monitorings.get_all_site_geometries", + id_sites_group=site_group_without_sites.id_sites_group, + ) + ) + json_resp = r.json + features = json_resp.get("features") + + assert r.status_code == 200 + assert features is None + + def test_get_all_site_geometries_filter_site_group( + self, sites, site_group_with_sites, monitorings_users + ): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + r = self.client.get( + url_for( + "monitorings.get_all_site_geometries", + id_sites_group=site_group_with_sites.id_sites_group, + ) + ) + json_resp = r.json + features = json_resp.get("features") + assert r.status_code == 200 + assert len(features) > 0 + + def test_get_all_site_geometries_filters( + self, + sites, + monitorings_users, + ): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + + # Test with user's id + r = self.client.get( + url_for( + "monitorings.get_all_site_geometries", + id_inventor=monitorings_users["user"].id_role, + ) + ) + json_resp = r.json + features = json_resp.get("features") + assert r.status_code == 200 + assert len(features) == len(sites) + + # Test with user's name + r = self.client.get( + url_for( + "monitorings.get_all_site_geometries", + id_inventor=monitorings_users["user"].nom_role, + ) + ) + json_resp = r.json + features = json_resp.get("features") + assert r.status_code == 200 + assert len(features) == len(sites) + + def test_get_all_site_geometries_filter_utils( + self, sites_with_data_typeutils, monitorings_users, users + ): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + # types_site = [s.types_site[0].id_nomenclature_type_site for s in sites_with_data_typeutils] + types_site = [s for s in sites_with_data_typeutils] + id_nomenclature_type_site = ( + sites_with_data_typeutils[types_site[0]].types_site[0].id_nomenclature_type_site + ) + r = self.client.get( + url_for( + "monitorings.get_all_site_geometries", + types_site=id_nomenclature_type_site, + cd_nom_test="Sonneur", + observers3=users["user"].nom_complet, + id_nomenclature_sex="Femelle", + ) + ) + json_resp = r.json + features = json_resp.get("features") + assert r.status_code == 200 + assert len(features) == 1 + r = self.client.get( + url_for( + "monitorings.get_all_site_geometries", + types_site=id_nomenclature_type_site, + cd_nom_test="Sonneur", + observers3=users["user"].nom_complet, + id_nomenclature_sex="Femelle", + multiple_cd_nom_test="Sonneur", + multiple_observers3=users["user"].nom_complet, + multiple_id_nomenclature_sex="Femelle", + ) + ) + json_resp = r.json + features = json_resp.get("features") + assert r.status_code == 200 + assert len(features) == 1 + + def test_get_all_site_geometries_filter_type_sites( + self, sites_with_data_typeutils, monitorings_users, users + ): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + # types_site = [s.types_site[0].id_nomenclature_type_site for s in sites_with_data_typeutils] + types_site = [s for s in sites_with_data_typeutils] + id_nomenclature_type_site = ( + sites_with_data_typeutils[types_site[0]].types_site[0].id_nomenclature_type_site + ) + r = self.client.get( + url_for("monitorings.get_all_site_geometries", types_site=id_nomenclature_type_site) + ) + json_resp = r.json + features = json_resp.get("features") + assert r.status_code == 200 + assert len(features) == 1 + nom_type_site = list(sites_with_data_typeutils.keys())[0] + r = self.client.get( + url_for( + "monitorings.get_all_site_geometries", + types_site=nom_type_site, + ) + ) + json_resp = r.json + features = json_resp.get("features") + assert r.status_code == 200 + assert len(features) == 1 + + # def test_get_module_by_id_base_site(self, sites, monitoring_module, monitorings_users): + + # set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + # site = list(sites.values())[0] + # id_base_site = site.id_base_site + + # r = self.client.get( + # url_for("monitorings.get_module_by_id_base_site", id_base_site=id_base_site) + # ) + # expected_modules = {monitoring_module.id_module} + # current_modules = {module["id_module"] for module in r.json} + # assert expected_modules.issubset(current_modules) + + def test_get_module_by_id_base_site_no_type_module( + self, sites, monitoring_module_wo_types_site, monitorings_users + ): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + site = list(sites.values())[0] + id_base_site = site.id_base_site + + r = self.client.get( + url_for("monitorings.get_module_by_id_base_site", id_base_site=id_base_site) + ) + + expected_absent_modules = {monitoring_module_wo_types_site.id_module} + current_modules = {module["id_module"] for module in r.json} + assert expected_absent_modules.isdisjoint(current_modules) + + def test_get_module_by_id_base_site_no_type_site( + self, sites, monitoring_module, monitorings_users + ): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + id_base_site = sites["no-type"].id_base_site + + r = self.client.get( + url_for("monitorings.get_module_by_id_base_site", id_base_site=id_base_site) + ) + expected_modules = {monitoring_module.id_module} + current_modules = {module["id_module"] for module in r.json} + assert expected_modules.isdisjoint(current_modules) + + def test_get_module_sites(self, monitoring_module, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + module_code = "TEST" + r = self.client.get(url_for("monitorings.get_module_sites", module_code=module_code)) + print(url_for("monitorings.get_module_sites", module_code=module_code)) + print(r.json) + assert r.json["module_code"] == module_code + + def test_get_types_site_by_label(self, types_site, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + schema = BibTypeSiteSchema() + mock_db_type_site = [schema.dump(type) for type in types_site.values()] + string_contains = "e" + string_missing = "a" + + query_string = { + "limit": 100, + "page": 1, + "sort_label": "label_fr", + "sort_dir": "asc", + "label_fr": string_contains, + } + r = self.client.get( + url_for("monitorings.get_types_site_by_label"), query_string=query_string + ) + assert all([string_contains in item["label"] for item in r.json["items"]]) + assert all([type in r.json["items"] for type in mock_db_type_site]) + + query_string["label_fr"] = string_missing + r = self.client.get( + url_for("monitorings.get_types_site_by_label"), query_string=query_string + ) + assert all([type not in r.json["items"] for type in mock_db_type_site]) + + def test_post_sites( + self, site_to_post_with_types, types_site, site_group_without_sites, monitorings_users + ): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + response = self.client.post( + url_for("monitorings.post_sites"), data=site_to_post_with_types + ) + assert response.status_code == 201 + + obj_created = response.json + res = db.get_or_404(TMonitoringSites, obj_created["id"]) + assert ( + res.as_dict()["base_site_name"] + == site_to_post_with_types["properties"]["base_site_name"] + ) + + assert set(res.types_site) == set([ts for k, ts in types_site.items()]) + + def test_delete_site(self, sites, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + site = list(sites.values())[0] + id_base_site = site.id_base_site + r = self.client.delete(url_for("monitorings.delete_site", _id=id_base_site)) + + assert r.json["success"] == "Item is successfully deleted" + with pytest.raises(Exception) as e: + db.get_or_404(TMonitoringSites, id_base_site) + assert "404 Not Found" in str(e.value) diff --git a/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py b/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py new file mode 100644 index 000000000..c56d03545 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py @@ -0,0 +1,90 @@ +import pytest + +from flask import url_for + +from sqlalchemy import select + +from geonature.utils.env import db + +from pypnusershub.tests.utils import set_logged_user_cookie + +from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups +from gn_module_monitoring.monitoring.schemas import MonitoringSitesGroupsSchema +from gn_module_monitoring.tests.fixtures.generic import * + + +@pytest.mark.usefixtures("client_class", "temporary_transaction") +class TestSitesGroups: + def test_get_sites_group_by_id(self, sites_groups, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + sites_group = list(sites_groups.values())[0] + id_sites_group = sites_group.id_sites_group + r = self.client.get( + url_for("monitorings.get_sites_group_by_id", id_sites_group=id_sites_group) + ) + + assert r.json["id_sites_group"] == id_sites_group + assert r.json["sites_group_name"] == sites_group.sites_group_name + + def test_get_sites_groups(self, sites_groups, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + r = self.client.get(url_for("monitorings.get_sites_groups")) + + assert r.json["count"] >= len(sites_groups) + + sites_group_response = r.json["items"] + for s in sites_group_response: + s.pop("cruved") + + assert all( + [ + MonitoringSitesGroupsSchema().dump(group) in sites_group_response + for group in sites_groups.values() + ] + ) + + def test_get_sites_groups_filter_name(self, sites_groups, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + name, name_not_present = list(sites_groups.keys()) + schema = MonitoringSitesGroupsSchema() + + r = self.client.get( + url_for("monitorings.get_sites_groups"), query_string={"sites_group_name": name} + ) + + assert r.json["count"] >= 1 + json_sites_groups = r.json["items"] + + # Suppression du cruved + for s in json_sites_groups: + s.pop("cruved") + + assert schema.dump(sites_groups[name]) in json_sites_groups + assert schema.dump(sites_groups[name_not_present]) not in json_sites_groups + + def test_serialize_sites_groups(self, sites_groups, sites): + groups = db.session.scalars( + select(TMonitoringSitesGroups).where( + TMonitoringSitesGroups.id_sites_group.in_( + [s.id_sites_group for s in sites_groups.values()] + ) + ) + ).all() + schema = MonitoringSitesGroupsSchema() + assert [schema.dump(site) for site in groups] + + def test_get_sites_groups_geometries(self, sites, site_group_with_sites, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + r = self.client.get(url_for("monitorings.get_sites_group_geometries")) + + json_resp = r.json + features = json_resp.get("features") + assert r.content_type == "application/json" + assert json_resp.get("type") == "FeatureCollection" + assert len(features) >= 1 + id_ = [ + obj["properties"] + for obj in features + if obj["properties"]["sites_group_name"] == site_group_with_sites.sites_group_name + ][0]["id_sites_group"] + assert id_ == site_group_with_sites.id_sites_group diff --git a/backend/gn_module_monitoring/tests/test_routes/test_visit.py b/backend/gn_module_monitoring/tests/test_routes/test_visit.py new file mode 100644 index 000000000..308a76bee --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_routes/test_visit.py @@ -0,0 +1,36 @@ +import pytest + +from flask import url_for + +from pypnusershub.tests.utils import set_logged_user_cookie + +from gn_module_monitoring.tests.fixtures.generic import * + + +@pytest.mark.usefixtures("client_class", "temporary_transaction") +class TestVisits: + def test_get_visits(self, visits, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + r = self.client.get( + url_for( + "monitorings.get_visits", + ) + ) + + expected_visits = {visit.id_base_visit for visit in visits} + current_visits = {visit["id_base_visit"] for visit in r.json["items"]} + assert expected_visits.issubset(current_visits) + assert all(visit["module"] is not None for visit in r.json["items"]) + + def test_get_visits_with_site(self, visits, sites, monitorings_users): + set_logged_user_cookie(self.client, monitorings_users["admin_user"]) + site = list(sites.values())[0] + + r = self.client.get(url_for("monitorings.get_visits", id_base_site=site.id_base_site)) + + expected_visits = { + visit.id_base_visit for visit in visits if visit.id_base_site == site.id_base_site + } + current_visits = {visit["id_base_visit"] for visit in r.json["items"]} + + assert expected_visits.issubset(current_visits) diff --git a/backend/gn_module_monitoring/tests/test_true.py b/backend/gn_module_monitoring/tests/test_true.py deleted file mode 100644 index 082867288..000000000 --- a/backend/gn_module_monitoring/tests/test_true.py +++ /dev/null @@ -1,6 +0,0 @@ -import pytest - - -class Tests: - def test_true_is_true(self): - assert True == True diff --git a/backend/gn_module_monitoring/tests/test_utils/__init__.py b/backend/gn_module_monitoring/tests/test_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/test_utils/test_errors.py b/backend/gn_module_monitoring/tests/test_utils/test_errors.py new file mode 100644 index 000000000..17b42aedb --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_utils/test_errors.py @@ -0,0 +1,20 @@ +import pytest +from gn_module_monitoring.utils.errors.errorHandler import InvalidUsage + + +def test_invalid_usage(): + with pytest.raises(InvalidUsage) as error: + raise InvalidUsage("this is an invalid usage") + assert error.type == InvalidUsage + assert ( + str(error.value) + == "Error 400, Message: this is an invalid usage, raised error: InvalidUsage" + ) + assert error.value.to_dict() == ( + { + "message": "this is an invalid usage", + "payload": None, + "status_code": 400, + }, + 400, + ) diff --git a/backend/gn_module_monitoring/tests/test_utils/test_routes.py b/backend/gn_module_monitoring/tests/test_utils/test_routes.py new file mode 100644 index 000000000..6faedda5f --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_utils/test_routes.py @@ -0,0 +1,29 @@ +import pytest +from werkzeug.datastructures import MultiDict + +from sqlalchemy import select + +from gn_module_monitoring.monitoring.models import TMonitoringSites +from gn_module_monitoring.monitoring.schemas import MonitoringSitesSchema +from gn_module_monitoring.utils.routes import get_limit_page, paginate + + +@pytest.mark.parametrize("limit, page", [("1", "2"), (1, 2), ("1", 2), (1, "2")]) +def test_get_limit_page(limit, page): + multi_dict = MultiDict([("limit", limit), ("page", page)]) + + comp_limit, comp_page = get_limit_page(params=multi_dict) + + assert isinstance(comp_limit, int) + assert isinstance(comp_page, int) + + +def test_paginate(sites): + limit = 1 + page = 2 + + res = paginate( + query=select(TMonitoringSites), schema=MonitoringSitesSchema, limit=limit, page=page + ) + + assert res.json["page"] == page diff --git a/backend/gn_module_monitoring/tests/test_utils/test_utils.py b/backend/gn_module_monitoring/tests/test_utils/test_utils.py new file mode 100644 index 000000000..a9bfe4550 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_utils/test_utils.py @@ -0,0 +1,10 @@ +import pytest +from gn_module_monitoring.utils.utils import to_int + + +def test_to_int_valid(): + assert to_int("3") == 3 + + +def test_to_int_invalid(): + assert to_int("hello") == None diff --git a/backend/gn_module_monitoring/utils/errors/__init__.py b/backend/gn_module_monitoring/utils/errors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/utils/errors/errorHandler.py b/backend/gn_module_monitoring/utils/errors/errorHandler.py new file mode 100644 index 000000000..3cf4b0363 --- /dev/null +++ b/backend/gn_module_monitoring/utils/errors/errorHandler.py @@ -0,0 +1,16 @@ +from geonature.utils.errors import GeonatureApiError + + +class InvalidUsage(GeonatureApiError): + def __init__(self, message, status_code=400, payload=None): + GeonatureApiError.__init__(self, message, status_code) + self.message = message + self.status_code = status_code + self.payload = payload + + def to_dict(self): + rv = {} + rv["payload"] = self.payload + rv["message"] = self.message + rv["status_code"] = self.status_code + return (rv, self.status_code) diff --git a/backend/gn_module_monitoring/utils/routes.py b/backend/gn_module_monitoring/utils/routes.py new file mode 100644 index 000000000..1f79e106d --- /dev/null +++ b/backend/gn_module_monitoring/utils/routes.py @@ -0,0 +1,251 @@ +from flask import Response, g +from flask.json import jsonify + +from typing import Tuple +from marshmallow import Schema +from werkzeug.datastructures import MultiDict +from sqlalchemy import cast, func, text, select, and_ +from sqlalchemy.dialects.postgresql import JSON +from sqlalchemy.orm import load_only +from sqlalchemy.sql.expression import Select + +from geonature.utils.env import DB +from geonature.core.gn_permissions.models import PermObject, PermissionAvailable +from geonature.core.gn_monitoring.models import BibTypeSite +from geonature.utils.errors import GeoNatureError + +from pypnusershub.db.models import User +from pypnnomenclature.models import TNomenclatures + +from gn_module_monitoring.monitoring.models import ( + TMonitoringSites, + TMonitoringSitesGroups, + cor_site_type, + TBaseSites, + cor_module_type, + TModules, +) +from gn_module_monitoring.monitoring.schemas import paginate_schema + + +def get_limit_page(params: MultiDict) -> Tuple[int]: + return int(params.pop("limit", 50)), int(params.pop("page", 1)) + + +def get_sort(params: MultiDict, default_sort: str, default_direction) -> Tuple[str]: + return params.pop("sort", default_sort), params.pop("sort_dir", default_direction) + + +def paginate(query: Select, schema: Schema, limit: int, page: int) -> Response: + result = DB.paginate(query, page=page, per_page=limit, error_out=False) + pagination_schema = paginate_schema(schema) + data = pagination_schema().dump( + dict(items=result.items, count=result.total, limit=limit, page=page) + ) + return jsonify(data) + + +def paginate_scope( + query: Select, schema: Schema, limit: int, page: int, object_code=None +) -> Response: + result = DB.paginate(query, page=page, per_page=limit, error_out=False) + + pagination_schema = paginate_schema(schema) + + datas_allowed = pagination_schema().dump( + dict(items=result.items, count=result.total, limit=limit, page=page) + ) + cruved_item_dict = get_objet_with_permission_boolean(result, object_code=object_code) + for cruved_item in cruved_item_dict: + for i, data in enumerate(datas_allowed["items"]): + if data[data["pk"]] == cruved_item[data["pk"]]: + datas_allowed["items"][i]["cruved"] = cruved_item["cruved"] + return jsonify(datas_allowed) + + +def filter_params(model, query: Select, params: MultiDict) -> Select: + if len(params) == 0: + return query + + if getattr(model, "filter_by_params", None): + return model.filter_by_params(query=query, params=params) + else: + raise GeoNatureError("filter_params : La requête n'a pas de méthode filter_by_params") + + +def sort(model, query: Select, sort: str, sort_dir: str) -> Select: + if sort_dir not in ["desc", "asc"]: + return query + + if getattr(query, "sort", None): + return query.sort(label=sort, direction=sort_dir) + + if getattr(model, sort, None): + order_by = getattr(model, sort) + if sort_dir == "desc": + order_by = order_by.desc() + return query.order_by(order_by) + + +def geojson_query(subquery) -> bytes: + subquery_name = "q" + subquery = subquery.alias(subquery_name) + query = select( + func.json_build_object( + text("'type'"), + text("'FeatureCollection'"), + text("'features'"), + func.json_agg(cast(func.st_asgeojson(subquery), JSON)), + ) + ) + result = DB.session.execute(query.limit(1)).first() + if len(result) > 0: + return result[0] + return b"" + + +def get_sites_groups_from_module_id(module_id: int): + query = ( + select(TMonitoringSitesGroups) + .options( + # Load(TMonitoringSitesGroups).raiseload("*"), + load_only(TMonitoringSitesGroups.id_sites_group) + ) + .join( + TMonitoringSites, + TMonitoringSites.id_sites_group == TMonitoringSitesGroups.id_sites_group, + ) + .join(cor_site_type, cor_site_type.c.id_base_site == TBaseSites.id_base_site) + .join( + BibTypeSite, + BibTypeSite.id_nomenclature_type_site == cor_site_type.c.id_type_site, + ) + .join( + cor_module_type, + cor_module_type.c.id_type_site == BibTypeSite.id_nomenclature_type_site, + ) + .join(TModules, TModules.id_module == cor_module_type.c.id_module) + .where(TModules.id_module == module_id) + ) + return DB.session.scalars(query).all() + + +def query_all_types_site_from_site_id(id_site: int): + query = ( + select(BibTypeSite) + .join( + cor_site_type, + BibTypeSite.id_nomenclature_type_site == cor_site_type.c.id_type_site, + ) + .join(TBaseSites, cor_site_type.c.id_base_site == TBaseSites.id_base_site) + .where(cor_site_type.c.id_base_site == id_site) + ) + + return DB.session.scalars(query).unique().all() + + +def query_all_types_site_from_module_id(id_module: int = None): + query = select(BibTypeSite) + if id_module: + query = query.join( + cor_module_type, + BibTypeSite.id_nomenclature_type_site == cor_module_type.c.id_type_site, + ) + query = query.where(cor_module_type.c.id_module == id_module) + return DB.session.scalars(query).unique().all() + + +def filter_according_to_column_type_for_site(query, params): + if "types_site" in params: + params_types_site = params.pop("types_site") + query = ( + query.join(TMonitoringSites.types_site) + .join(BibTypeSite.nomenclature) + .where(TNomenclatures.label_fr.ilike(f"%{params_types_site}%")) + ) + elif "id_inventor" in params: + params_inventor = params.pop("id_inventor") + query = query.join( + User, + User.id_role == TMonitoringSites.id_inventor, + ).where(User.nom_complet.ilike(f"%{params_inventor}%")) + if len(params) != 0: + query = filter_params(TMonitoringSites, query=query, params=params) + + return query + + +def sort_according_to_column_type_for_site(query, sort_label, sort_dir): + if sort_label == "types_site": + if sort_dir == "asc": + query = query.order_by(TNomenclatures.label_fr.asc()) + else: + query = query.order_by(TNomenclatures.label_fr.desc()) + elif sort_label == "id_inventor": + if sort_dir == "asc": + query = query.order_by(User.nom_complet.asc()) + else: + query = query.order_by(User.nom_complet.desc()) + else: + query = sort(TMonitoringSites, query=query, sort=sort_label, sort_dir=sort_dir) + return query + + +def get_object_list_monitorings(): + """ + récupère objets permissions liés au module MONITORINGS + + :return: + """ + try: + object_list_monitorings = DB.session.execute( + select( + PermObject.code_object, + ) + .join(PermissionAvailable, PermissionAvailable.id_object == PermObject.id_object) + .join( + TModules, + and_( + TModules.id_module == PermissionAvailable.id_module, + TModules.module_code == "MONITORINGS", + ), + ) + .group_by(PermObject.code_object) + ).all() + return object_list_monitorings + except Exception as e: + raise GeoNatureError("MONITORINGS - get_object_list_monitorings : {}".format(str(e))) + + +def get_objet_with_permission_boolean( + objects, depth: int = 0, module_code=None, object_code=None, id_role=None +): + if id_role is None: + id_role = g.current_user.id_role + objects_out = [] + for object in objects: + if module_code: + cruved_object = object._get_cruved_scope( + module_code=module_code, object_code=object_code + ) + elif hasattr(object, "module"): + cruved_object = object._get_cruved_scope( + module_code=object.module.module_code, object_code=object_code + ) + elif hasattr(object, "module_code"): + cruved_object = object._get_cruved_scope( + module_code=object.module_code, object_code=object_code + ) + else: + cruved_object = object._get_cruved_scope(object_code=object_code) + object_out = object.as_dict(depth=depth) + + if hasattr(object, "module_code"): + object_out["cruved"] = object.get_permission_by_action( + module_code=object.module_code, object_code=object_code + ) + else: + object_out["cruved"] = object.has_permission(cruved_object=cruved_object) + objects_out.append(object_out) + + return objects_out diff --git a/backend/gn_module_monitoring/utils/utils.py b/backend/gn_module_monitoring/utils/utils.py index 2cdc00b98..31754697c 100644 --- a/backend/gn_module_monitoring/utils/utils.py +++ b/backend/gn_module_monitoring/utils/utils.py @@ -1,7 +1,28 @@ +import collections.abc + + def to_int(s): try: return int(s) - except ValueError: - return None except Exception: return None + + +def dict_deep_update(dct, merge_dct): + """Recursive dict merge. Inspired by :meth:``dict.update()``, instead of + updating only top-level keys, dict_merge recurses down into dicts nested + to an arbitrary depth, updating keys. The ``merge_dct`` is merged into + ``dct``. + :param dct: dict onto which the merge is executed + :param merge_dct: dct merged into dct + :return: None + """ + for k, v in merge_dct.items(): + if ( + k in dct + and isinstance(dct[k], dict) + and isinstance(merge_dct[k], collections.abc.Mapping) + ): + dict_deep_update(dct[k], merge_dct[k]) + else: + dct[k] = merge_dct[k] diff --git a/backend/gn_module_monitoring/utils/utilsjsonschema.py b/backend/gn_module_monitoring/utils/utilsjsonschema.py deleted file mode 100644 index e784a4edd..000000000 --- a/backend/gn_module_monitoring/utils/utilsjsonschema.py +++ /dev/null @@ -1,48 +0,0 @@ -from json import load -from jsonschema import validate -from jsonschema.exceptions import ValidationError - - -def validable(cls): - """ - Décorateur de classe - Ajoute une fonction de valider des jsonschema sur un colonne JSONB - """ - - def is_valid(self, col_name, file_schema_path): - """ - Function qui renvoie True si la colonne indiquée par colname est - conforme au schema contenu dans le fichier de chemin file_schema_path, - False Sinon. - Renvoie None si la colonne référencée par col_name n'existe pas ou n' - est pas de type JSONB. - - :param col_name: Nom de la colonne - :param file_schema_path: chemin absolu du fichier contenant le schema - """ - - # test col_name valide - data = getattr(self, col_name) - if not data: - return - - # test type JSONB - type = getattr(cls.__mapper__.c, col_name).type - - if not str(type)[:5] == "JSONB": - return - - # test si l'objet json est conforme au schema - with open(file_schema_path, "r") as f: - schema = load(f) - - try: - data = getattr(self, col_name) - validate(instance=data, schema=schema) - return True - - except ValidationError: - return False - - cls.is_valid = is_valid - return cls diff --git a/conftest.py b/conftest.py new file mode 100644 index 000000000..92ab61efc --- /dev/null +++ b/conftest.py @@ -0,0 +1,10 @@ +from geonature.tests.fixtures import * +from geonature.tests.fixtures import _session, app, users + +pytest_plugins = [ + "gn_module_monitoring.tests.fixtures.module", + "gn_module_monitoring.tests.fixtures.site", + "gn_module_monitoring.tests.fixtures.sites_groups", + "gn_module_monitoring.tests.fixtures.type_site", + "gn_module_monitoring.tests.fixtures.visit", +] diff --git a/contrib/test/exports/csv/export.sql b/contrib/test/exports/csv/export.sql new file mode 100644 index 000000000..c9aa590f0 --- /dev/null +++ b/contrib/test/exports/csv/export.sql @@ -0,0 +1,23 @@ + +CREATE +OR REPLACE VIEW gn_monitoring.v_export_test_sites AS WITH MOD AS ( + SELECT + * + FROM + gn_commons.t_modules AS tm + WHERE + module_code = :module_code +) +SELECT + tbs.base_site_code, + st_x(tbs.geom) AS longitude, + st_y(tbs.geom) AS latitude +FROM + gn_monitoring.t_base_sites AS tbs + JOIN gn_monitoring.cor_site_module AS csm ON csm.id_base_site = tbs.id_base_site + AND csm.id_module = ( + SELECT + id_module + FROM + mod + ); \ No newline at end of file diff --git a/contrib/test/nomenclature.json b/contrib/test/nomenclature.json index 0043df5ac..393dd1e41 100644 --- a/contrib/test/nomenclature.json +++ b/contrib/test/nomenclature.json @@ -20,6 +20,13 @@ "mnemonique": "Mauvais", "label_default": "Mauvais temps", "definition_default": "Mauvais temps (test)" + }, + { + "type": "TEST_UNKWONW_TYPE", + "cd_nomenclature": "0", + "mnemonique": "0", + "label_default": "Nomenclature avec un type inexistant", + "definition_default": "Nomenclature avec un type inexistant" } ] } diff --git a/contrib/test/synthese.sql b/contrib/test/synthese.sql index 58839b38c..3bcaac11b 100644 --- a/contrib/test/synthese.sql +++ b/contrib/test/synthese.sql @@ -13,14 +13,10 @@ -- - choisir les valeurs de champs de nomenclatures qui seront propres au modules --- ce fichier contient une variable :module_code (ou :'module_code') +-- ce fichier contient une variable :module_code -- utiliser psql avec l'option -v module_code=/config/monitorings` vers le dossier `media/monitorings` de GeoNature. Cela permet d'homogéneiser et centraliser la configuration de GeoNature et de ses sous-modules, de permettre la dockerisation du module Monitoring et de simplifier sa mise à jour (#224) -* Installation des sous-modules en deux temps -* Passage de la documentation au format `markdown` (#227) -* Suppression du script obsolète `update_views.sh` -* Commande d'installation d'un sous-module : ajout de la liste des modules installés et disponibles +- Utilisation de la gestion dynamique de la configuration de GeoNature (#224) +- Les dossiers de configuration des sous-modules sont déplacés du dossier `/config/monitorings` vers le dossier `media/monitorings` de GeoNature. Cela permet d'homogéneiser et centraliser la configuration de GeoNature et de ses sous-modules, de permettre la dockerisation du module Monitoring et de simplifier sa mise à jour (#224) +- Installation des sous-modules en deux temps +- Passage de la documentation au format `markdown` (#227) +- Suppression du script obsolète `update_views.sh` +- Commande d'installation d'un sous-module : ajout de la liste des modules installés et disponibles **⚠️ Notes de version** -* Veuillez déplacer les configurations des sous-modules déjà existants depuis le dossier `/config/monitoring` vers le dossier `media` de GeoNature : +- Veuillez déplacer les configurations des sous-modules déjà existants depuis le dossier `/config/monitoring` vers le dossier `media` de GeoNature : ``` cp -R ~/gn_module_monitoring/config/monitoring/* ~/geonature/backend/media/monitorings @@ -80,39 +118,38 @@ Nécessite GeoNature version 2.12.0 (ou plus) Adapter cette commande si le répertoire `medias` de GeoNature est différent de l'exemple ci-dessus. Attention aux `s` à la fin de monitoring (le premier sans le deuxième avec). -* L'installation des sous-modules se fait désormais en deux temps : +- L'installation des sous-modules se fait désormais en deux temps : - * Copie du répertoire de configuration - ```sh - cp /backend/media/monitorings/ - ``` - * Installation du sous-module avec la commande dédiée - ```sh - geonature monitorings install - ``` + * Copie du répertoire de configuration + ```sh + cp /backend/media/monitorings/ + ``` + * Installation du sous-module avec la commande dédiée + ```sh + geonature monitorings install + ``` -0.5.0 (2023-03-29) ------------------- +## 0.5.0 (2023-03-29) Nécessite GeoNature version 2.12.0 (ou plus) **🚀 Nouveautés** -* Compatibilité avec GeoNature 2.12 (Passage à la version 15 d'Angular et révision des permissions) -* Centralisation de la configuration du module dans le dossier de configuration de GeoNature - * Pour le fichier de ``/config/monitorings_config.toml`` (facultatif car non utilisé) - * et le dossier de configuration des sous-modules ``/config/monitorings`` -* Permissions : utilisation du décorateur de route classique ``check_cruved_scope`` -* Amélioration de l'affichage des images sur la page listant les sous-modules (#214) -* Remplacement du composant `datalist` par le composant `dataset` pour le champs de selection du JDD de la visite, dans la configuration générique des sous-modules +- Compatibilité avec GeoNature 2.12 (Passage à la version 15 d'Angular et révision des permissions) +- Centralisation de la configuration du module dans le dossier de configuration de GeoNature + - Pour le fichier de `/config/monitorings_config.toml` (facultatif car non utilisé) + - et le dossier de configuration des sous-modules `/config/monitorings` +- Permissions : utilisation du décorateur de route classique `check_cruved_scope` +- Amélioration de l'affichage des images sur la page listant les sous-modules (#214) +- Remplacement du composant `datalist` par le composant `dataset` pour le champs de selection du JDD de la visite, dans la configuration générique des sous-modules **🐛 Corrections** -* Correction de l'API ``get_util_from_id_api`` en traitant les ``id`` en ``str`` (#175) +- Correction de l'API `get_util_from_id_api` en traitant les `id` en `str` (#175) **⚠️ Notes de version** -* L'utilisation du widget ``datalist`` pour les jeux de données est à proscrire. Si vous utilisez ce composant dans vos fichiers de configuration, il faut les modifier en remplaçant par le widget ``dataset``. +- L'utilisation du widget `datalist` pour les jeux de données est à proscrire. Si vous utilisez ce composant dans vos fichiers de configuration, il faut les modifier en remplaçant par le widget `dataset`. ```json "id_dataset": { @@ -124,20 +161,18 @@ Nécessite GeoNature version 2.12.0 (ou plus) }, ``` - -0.4.1 (2023-02-05) -================== +## 0.4.1 (2023-02-05) **🚀 Nouveautés** -* Configuration des exports pour rendre optionnelle la sélection du - jeu de données avec le nouveau paramètre `filter_dataset` (#158) +- Configuration des exports pour rendre optionnelle la sélection du + jeu de données avec le nouveau paramètre `filter_dataset` (#158) **🐛 Corrections** -* Amélioration des performances du chargement des observations (#142) -* Correction du modèle "Observation détail" qui permet d'ajouter - des informations sous le niveau observation +- Amélioration des performances du chargement des observations (#142) +- Correction du modèle "Observation détail" qui permet d'ajouter + des informations sous le niveau observation **⚠️ Notes de version** @@ -145,6 +180,7 @@ Si vous souhaitez que les exports soient filtrables par jeux de données, il faut rajouter le nouveau paramètre `filter_dataset` dans la variable `export_csv`, définie à `true` au niveau de la configuration des modules concernés (dans leur fichier `module.json`). Exemple : + ```json "export_csv": [ { "label": "Format standard CSV", "type":"csv" , "method": "standard" , "filter_dataset": true}, @@ -152,226 +188,220 @@ concernés (dans leur fichier `module.json`). Exemple : ], ``` -0.4.0 (2022-12-21) -================== +## 0.4.0 (2022-12-21) Nécessite la version 2.11.0 (ou plus) de GeoNature. **🚀 Nouveautés** -* Packaging du module (#190) -* Gestion de la base de données avec Alembic (#190) -* Améliorations du typage frontend +- Packaging du module (#190) +- Gestion de la base de données avec Alembic (#190) +- Améliorations du typage frontend **🐛 Corrections** -* Correction du marqueur Leaflet non visible lors de la création d'un - point sur la carte (#187) -* Peuplement du champs `gn_monitoring.t_module_complements.type` avec - la valeur `monitoring_module` pour les sous-modules de Monitoring - (#193) -* Correction de l'utilisation des modèles de TaxRef -* Suppression de l'usage de `MODULE_URL` dans la configuration du - module () +- Correction du marqueur Leaflet non visible lors de la création d'un + point sur la carte (#187) +- Peuplement du champs `gn_monitoring.t_module_complements.type` avec + la valeur `monitoring_module` pour les sous-modules de Monitoring + (#193) +- Correction de l'utilisation des modèles de TaxRef +- Suppression de l'usage de `MODULE_URL` dans la configuration du + module () **⚠️ Notes de version** Si vous mettez à jour le module, il vous faut passer à Alembic. Pour cela, une fois la version 2.11 (ou plus) de GeoNature installée : -* Entrer dans le virtualenv : +- Entrer dans le virtualenv : ```sh source ~/geonature/backend/venv/bin/activate ``` -* Installer la nouvelle version de Monitoring avec le paramètre - `--upgrade-db=false` : +- Installer la nouvelle version de Monitoring avec le paramètre + `--upgrade-db=false` : ```sh geonature install-gn-module --upgrade-db=false MONITORINGS ``` -* Indiquer à Alembic que votre base de données est en version 0.3.0 : +- Indiquer à Alembic que votre base de données est en version 0.3.0 : ```sh geonature db stamp 362cf9d504ec # monitorings 0.3.0 ``` -* Mettre à jour la base de données en version 0.4.0 : +- Mettre à jour la base de données en version 0.4.0 : ```sh geonature db upgrade monitorings@head ``` -0.3.0 (2022-11-02) -================== +## 0.3.0 (2022-11-02) Nécessite la version 2.10.0 (ou plus) de GeoNature. **🚀 Nouveautés** -* Compatibilité avec Angular version 12, mis à jour dans la version - 2.10.0 de GeoNature (#135) -* Ajout de la commande `synchronize_synthese` permettant de - synchroniser les données d'un sous-module vers la Synthèse (#176) -* Tri de la liste des sous-modules par nom sur la page d'accueil du - module (#182) -* Ajout des champs `altitude_min` et `altitude_max` dans les - informations affichables au niveau des sites (`generic/site.json`) - (#170) -* Calcul de la géometrie des groupes de sites basculé au niveau - backend (avec la fonction `ST_ConvexHull` de PostGIS qui prend - l'enveloppe convexe des sites du groupe - #149) -* Amélioration du style des informations sur les fiches des objets - (#151) -* Ajout d'un paramètre `redirect_to_parent` au niveau de - `observation.json` permettant de rediriger vers la fiche de la - visite à la fin de la saisie en mode \"Enchainer les saisies\", - plutôt que vers la fiche de l'observation (#152) -* Ajout de la commande `process_all` permettant de régénérer toute la - configuration d'un sous-module quand il est déjà installé en base - de données -* Possibilité de transmettre la valeur du code du module dans - l'export (#168) +- Compatibilité avec Angular version 12, mis à jour dans la version + 2.10.0 de GeoNature (#135) +- Ajout de la commande `synchronize_synthese` permettant de + synchroniser les données d'un sous-module vers la Synthèse (#176) +- Tri de la liste des sous-modules par nom sur la page d'accueil du + module (#182) +- Ajout des champs `altitude_min` et `altitude_max` dans les + informations affichables au niveau des sites (`generic/site.json`) + (#170) +- Calcul de la géometrie des groupes de sites basculé au niveau + backend (avec la fonction `ST_ConvexHull` de PostGIS qui prend + l'enveloppe convexe des sites du groupe - #149) +- Amélioration du style des informations sur les fiches des objets + (#151) +- Ajout d'un paramètre `redirect_to_parent` au niveau de + `observation.json` permettant de rediriger vers la fiche de la + visite à la fin de la saisie en mode \"Enchainer les saisies\", + plutôt que vers la fiche de l'observation (#152) +- Ajout de la commande `process_all` permettant de régénérer toute la + configuration d'un sous-module quand il est déjà installé en base + de données +- Possibilité de transmettre la valeur du code du module dans + l'export (#168) **🐛 Corrections** -* Correction de la hauteur aléatoire du container principal (#146) -* Correction du zoom sur un objet de la carte au clic sur l'objet - dans la liste (#149) -* Correction de l'affichage des tooltips quand la géométrie est un - polygone (#159) -* Correction de la transformation des chaines de caractère en date - (#170) -* Suppression de l'alias `@librairies` (#178) +- Correction de la hauteur aléatoire du container principal (#146) +- Correction du zoom sur un objet de la carte au clic sur l'objet + dans la liste (#149) +- Correction de l'affichage des tooltips quand la géométrie est un + polygone (#159) +- Correction de la transformation des chaines de caractère en date + (#170) +- Suppression de l'alias `@librairies` (#178) -0.2.10 (2022-03-02) -=================== +## 0.2.10 (2022-03-02) Compatible avec GeoNature version 2.9.2 maximum. **🐛 Corrections** -* Reprise de la config pour les champs de jeux de données et les - observateurs -* Prise en compte du changement de l'api pour les jdd pour le choix - des jdd de l'export +- Reprise de la config pour les champs de jeux de données et les + observateurs +- Prise en compte du changement de l'api pour les jdd pour le choix + des jdd de l'export -0.2.9 (2022-01-13) -================== +## 0.2.9 (2022-01-13) Compatibilité avec GeoNature version 2.9.0 et plus. **🐛 Corrections** -* Correction de la vue `gn_monitoring.synthese_svo.sql` permettant - d'alimenter la Synthèse de GeoNature (#64) -* Reprise du composant de la liste déroulante de sélection des jeux de - données, suite au passage à `ng-select2` dans GeoNature 2.9.0 +- Correction de la vue `gn_monitoring.synthese_svo.sql` permettant + d'alimenter la Synthèse de GeoNature (#64) +- Reprise du composant de la liste déroulante de sélection des jeux de + données, suite au passage à `ng-select2` dans GeoNature 2.9.0 -0.2.8 (2021-12-10) -================== +## 0.2.8 (2021-12-10) **🐛 Corrections** -* Suppression du trigger `tri_meta_dates_change_t_module_complements` - dans le script d'installation du module (#118 et #120) -* Modification de la fonction contour des sites : - * Un contour pour chaque groupe de sites - * Prise en compte uniquement des sites visibles sur la carte (non - filtrés) dans le calcul -* Complément des notes de version de la 0.2.7 (#119 par \@maximetoma) -* Les modules POPAmphibien et POPReptile ont été déplacés dans le - dépot +- Suppression du trigger `tri_meta_dates_change_t_module_complements` + dans le script d'installation du module (#118 et #120) +- Modification de la fonction contour des sites : + - Un contour pour chaque groupe de sites + - Prise en compte uniquement des sites visibles sur la carte (non + filtrés) dans le calcul +- Complément des notes de version de la 0.2.7 (#119 par \@maximetoma) +- Les modules POPAmphibien et POPReptile ont été déplacés dans le + dépot -0.2.7 (2021-10-26) -================== +## 0.2.7 (2021-10-26) **⚠️ Notes de version** Si vous mettez à jour le module : -* Nouvelles commandes pour gérer et mettre à jour les exports `pdf` et - `csv` pour un module si `module_code` est précisé ou pour tous les - modules : +- Nouvelles commandes pour gérer et mettre à jour les exports `pdf` et + `csv` pour un module si `module_code` est précisé ou pour tous les + modules : **🚀 Nouveautés POPAmphibien - POPReptile** -* A partir de la version de GeoNature 2.7.5, les commandes de gestion - du module `monitorings` sont accessibles depuis la commande - `geonature monitorings` une fois que l'on a activé le `venv` -* Nouvelles commandes : - * `geonature monitorings process_export_pdf ` - * `geonature monitorings process_export_csv ` - * Pour gérer et mettre à jour les exports `pdf` et `csv` pour un - module si `module_code` est précisé ou pour tous les modules -* Ajout des sous-modules POPAmphibien et POPReptile (idéalement à - déplacer dans un autre dépôt) -* Possibilité de choisir la couleur du tableau pour les détails d'un - objet (champs `color` dans le fichier `.json`) -* Dans la partie map, possibilité de joindre les sites par des lignes - pour former automatiquement une aire et calculer sa superficie - * (si le nombre des points est supérieur à 2) - * configurable depuis l'édition du module ([dessin des groupe de - site]{.title-ref}) -* Possibilité de choisir l'icône du module dans le menu depuis - l'édition du module -* Export PDF configurables - * Bouton accessible depuis les détails -* Export CSV configurables - * Bouton accessible depuis les détails - * Modale pour choisir le JDD concerné par l'export +- A partir de la version de GeoNature 2.7.5, les commandes de gestion + du module `monitorings` sont accessibles depuis la commande + `geonature monitorings` une fois que l'on a activé le `venv` +- Nouvelles commandes : + - `geonature monitorings process_export_pdf ` + - `geonature monitorings process_export_csv ` + - Pour gérer et mettre à jour les exports `pdf` et `csv` pour un + module si `module_code` est précisé ou pour tous les modules +- Ajout des sous-modules POPAmphibien et POPReptile (idéalement à + déplacer dans un autre dépôt) +- Possibilité de choisir la couleur du tableau pour les détails d'un + objet (champs `color` dans le fichier `.json`) +- Dans la partie map, possibilité de joindre les sites par des lignes + pour former automatiquement une aire et calculer sa superficie + - (si le nombre des points est supérieur à 2) + - configurable depuis l'édition du module ([dessin des groupe de + site]{.title-ref}) +- Possibilité de choisir l'icône du module dans le menu depuis + l'édition du module +- Export PDF configurables + - Bouton accessible depuis les détails +- Export CSV configurables + - Bouton accessible depuis les détails + - Modale pour choisir le JDD concerné par l'export **🐛 Corrections** -* Rechargement de la configuration quand on modifie le module par le - formulaire d'édition +- Rechargement de la configuration quand on modifie le module par le + formulaire d'édition **⚠️ Notes de version** Si vous mettez à jour le module : -* Pour mettre à jour la base de données, il faut exécuter le fichier - `data/migration/migration_0.2.6_0.2.7.sql` -* Les exports nécessitent l'installation du module html2canvas. Il - peut être nécessaire de mettre à jour les modules js en suivant la - procédure suivante : +- Pour mettre à jour la base de données, il faut exécuter le fichier + `data/migration/migration_0.2.6_0.2.7.sql` +- Les exports nécessitent l'installation du module html2canvas. Il + peut être nécessaire de mettre à jour les modules js en suivant la + procédure suivante : ```sh cd path_to_geonature/frontend npm install external_modules/monitorings/frontend --no-save ``` -0.2.6 (2021-07-23) -================== + +## 0.2.6 (2021-07-23) Compatible avec GeoNature à partir de sa version 2.6.2 (dont GeoNature 2.8). **🚀 Nouveautés** -* Assets déplacés dans le dossier `static` - (`backend/static/external_assets/monitorings/`) de GeoNature (#102) -* Dans les listes d'objets, ajout d'un bouton plus pour accéder - directement à la création d'un enfant (#97) - * par exemple depuis la liste des sites on peut accéder - directement à la création d'une nouvelle visite +- Assets déplacés dans le dossier `static` + (`backend/static/external_assets/monitorings/`) de GeoNature (#102) +- Dans les listes d'objets, ajout d'un bouton plus pour accéder + directement à la création d'un enfant (#97) + - par exemple depuis la liste des sites on peut accéder + directement à la création d'une nouvelle visite **🐛 Corrections** -* Chargement des commandes Flask +- Chargement des commandes Flask **⚠️ Notes de version** -* L'emplacement des images des modules (dans la page d'accueil qui - permet de choisir un module) change. +- L'emplacement des images des modules (dans la page d'accueil qui + permet de choisir un module) change. Elles sont placées dans `backend/static/external_assets/monitorings/assets`, l'avantage est qu'il n'est plus nécessaire de rebuild le frontend à l'installation d'un sous module. -* Pour les mettre à jour, veuillez exécuter la commande suivante : +- Pour les mettre à jour, veuillez exécuter la commande suivante : ```sh source /home/`whoami`/geonature/backend/venv/bin/activate @@ -380,69 +410,66 @@ flask monitorings process_img ``` ou bien à partir de GeoNature 2.7.3 : + ```sh source /home/`whoami`/geonature/backend/venv/bin/activate export FLASK_APP=geonature geonature monitorings process_img ``` -0.2.5 (2021-07-12) -================== +## 0.2.5 (2021-07-12) **🐛 Corrections** Problème de route frontend (#100) -0.2.4 (2021-06-15) -================== +## 0.2.4 (2021-06-15) **🐛 Corrections** -* Problème de chainage des saisies -* Configuration de l'affichage des taxons `lb_nom` pris en compte +- Problème de chainage des saisies +- Configuration de l'affichage des taxons `lb_nom` pris en compte Version minimale de GeoNature nécessaire : 2.6.2 -0.2.3 (2021-04-01) -================== +## 0.2.3 (2021-04-01) Version minimale de GeoNature nécessaire : 2.5.5 **🐛 Corrections** -* Problème d'héritage des permissions (#78) +- Problème d'héritage des permissions (#78) **⚠️ Notes de version** Si vous mettez à jour le module : -* Suivez la procédure classique de mise à jour du module - (`docs/MAJ.rst`) +- Suivez la procédure classique de mise à jour du module + (`docs/MAJ.rst`) -0.2.2 (2021-03-22) -================== +## 0.2.2 (2021-03-22) -* Version minimale de GeoNature nécessaire : 2.5.5 +- Version minimale de GeoNature nécessaire : 2.5.5 **🚀 Nouveautés** -* Gestion des permissions par objet (site, groupe de site, visite, - observation) -* Interaction carte liste pour les groupes de site +- Gestion des permissions par objet (site, groupe de site, visite, + observation) +- Interaction carte liste pour les groupes de site **🐛 Corrections** -* Affichage des tooltips pour les objets cachés #76 +- Affichage des tooltips pour les objets cachés #76 **⚠️ Notes de version** Si vous mettez à jour le module : -* Pour mettre à jour la base de données, il faut exécuter le fichier - `data/migration/migration_0.2.1_0.2.2.sql` -* Suivez la procédure classique de mise à jour du module - (`docs/MAJ.rst`) -* Nettoyer des résidus liées à l'ancienne versions : +- Pour mettre à jour la base de données, il faut exécuter le fichier + `data/migration/migration_0.2.1_0.2.2.sql` +- Suivez la procédure classique de mise à jour du module + (`docs/MAJ.rst`) +- Nettoyer des résidus liées à l'ancienne versions : ```sh cd /home/`whoami`/geonature/frontend @@ -450,167 +477,164 @@ npm uninstall test npm ci /home/`whoami`/gn_module_monitoring/frontend/ --no-save ``` -0.2.1 (2021-01-14) -================== +## 0.2.1 (2021-01-14) -* Version minimale de GeoNature nécessaire : 2.5.5 +- Version minimale de GeoNature nécessaire : 2.5.5 **🚀 Nouveautés** -* Amélioration des groupes de sites (#24) -* Possibilité de charger un fichier GPS ou GeoJSON pour localiser un - site (#13) -* Alimentation massive de la synthèse depuis les données historiques - d'un sous-module de suivi (#38) -* Pouvoir définir des champs *dynamiques*, dont les attributs peuvent - dépendre des valeurs des autres composants (pour afficher un - composant en fonction de la valeur d'autres composants). Voir les - exemples dans le sous-module `test` -* Pouvoir definir une fonction `change` dans les fichiers - `.json` qui est exécutée à chaque changement du - formulaire. -* Champs data JSONB dans `module_complement` -* Gestion des objets qui apparraissent plusieurs fois dans `tree`. Un - objet peut avoir plusieurs [parents]{.title-ref} -* Améliorations grammaticales et possibilité de genrer les objets -* Choisir la possibilité d'afficher le bouton saisie multiple -* Par defaut pour les sites : - * `id_inventor` = `currentUser.id_role` si non défini - * `id_digitizer` = `currentUser.id_role` si non défini - * `first_use_date` = `` si non défini +- Amélioration des groupes de sites (#24) +- Possibilité de charger un fichier GPS ou GeoJSON pour localiser un + site (#13) +- Alimentation massive de la synthèse depuis les données historiques + d'un sous-module de suivi (#38) +- Pouvoir définir des champs _dynamiques_, dont les attributs peuvent + dépendre des valeurs des autres composants (pour afficher un + composant en fonction de la valeur d'autres composants). Voir les + exemples dans le sous-module `test` +- Pouvoir definir une fonction `change` dans les fichiers + `.json` qui est exécutée à chaque changement du + formulaire. +- Champs data JSONB dans `module_complement` +- Gestion des objets qui apparraissent plusieurs fois dans `tree`. Un + objet peut avoir plusieurs [parents]{.title-ref} +- Améliorations grammaticales et possibilité de genrer les objets +- Choisir la possibilité d'afficher le bouton saisie multiple +- Par defaut pour les sites : + - `id_inventor` = `currentUser.id_role` si non défini + - `id_digitizer` = `currentUser.id_role` si non défini + - `first_use_date` = `` si non défini **🐛 Corrections** -* Amélioration du titre (lisibilité et date francaise) -* Correction de la vue alimentant la synthèse -* Ajout du champs `base_site_description` au niveau de la - configuration générique des sites (#58) +- Amélioration du titre (lisibilité et date francaise) +- Correction de la vue alimentant la synthèse +- Ajout du champs `base_site_description` au niveau de la + configuration générique des sites (#58) **⚠️ Notes de version** Si vous mettez à jour le module : -* Pour mettre à jour la base de données, il faut exécuter le fichier - `data/migration/migration_0.2.0_0.2.1.sql` -* Pour mettre à jour la base de données, exécutez le fichier - `data/migration/migration_0.2.0_0.2.1.sql` -* Suivez la procédure classique de mise à jour du module - (`docs/MAJ.rst`) -* Les fichiers `config_data.json`, `custom.json`, et/ou la variable - [data]{.title-ref} dans `config.json` ne sont plus nécessaires et - ces données sont désormais gérées automatiquement depuis la - configuration. +- Pour mettre à jour la base de données, il faut exécuter le fichier + `data/migration/migration_0.2.0_0.2.1.sql` +- Pour mettre à jour la base de données, exécutez le fichier + `data/migration/migration_0.2.0_0.2.1.sql` +- Suivez la procédure classique de mise à jour du module + (`docs/MAJ.rst`) +- Les fichiers `config_data.json`, `custom.json`, et/ou la variable + [data]{.title-ref} dans `config.json` ne sont plus nécessaires et + ces données sont désormais gérées automatiquement depuis la + configuration. -0.2.0 (2020-10-23) -================== +## 0.2.0 (2020-10-23) Nécessite la version 2.5.2 de GeoNature minimum. **🚀 Nouveautés** -* Possibilité de renseigner le JDD à chaque visite - ([#30](https://github.com/PnX-SI/gn_module_monitoring/issues/30)) -* Possibilité pour les administrateurs d'associer les JDD à un - sous-module directement depuis l'accueil du sous-module - ([#30](https://github.com/PnX-SI/gn_module_monitoring/issues/30)) -* Possibilité de créer des groupes de sites (encore un peu jeune) - ([#24](https://github.com/PnX-SI/gn_module_monitoring/issues/24)) -* Possibilité de créer une visite directement après la création d'un - site, et d'une observation directement après la création d'une - visite - ([#28](https://github.com/PnX-SI/gn_module_monitoring/issues/28)) -* Redirection sur sa page de détail après la création d'un objet, - plutôt que sur la liste - ([#22](https://github.com/PnX-SI/gn_module_monitoring/issues/22)) -* Mise à jour du composant de gestion et d'affichage des médias -* Ajout d'un composant de liste modulable (`datalist`) pouvant - interroger une API, pouvant être utilisé pour les listes de taxons, - d'observateurs, de jdd, de nomenclatures, de sites, de groupes de - sites, etc... - ([#44](https://github.com/PnX-SI/gn_module_monitoring/issues/44)) -* Liste des observations : ajout d'un paramètre permettant - d'afficher le nom latin des taxons observés - ([#36](https://github.com/PnX-SI/gn_module_monitoring/issues/36)) -* Simplification de la procédure pour mettre les données dans la - synthèse (un fichier à copier, un bouton à cocher et possibilité de - customiser la vue pour un sous-module) -* Passage de la complexité des méthodes de mise en base des données et - de gestion des relation par liste d'`id` (observateurs, jdd du - module, correlations site module) vers le module - [Utils\_Flask\_SQLA]{.title-ref} (amélioration de la méthode - `from_dict` en mode récursif qui accepte des listes d'`id` et les - traduit en liste de modèles), (principalement dans - `backend/monitoring/serializer.py`) -* Suppression du fichier `custom.json` pour gérer son contenu dans les - nouveaux champs de la table `gn_monitoring.t_module_complements` - ([#43](https://github.com/PnX-SI/gn_module_monitoring/issues/43)) -* Clarification et remplacement des `module_path` et `module_code` - ([#40](https://github.com/PnX-SI/gn_module_monitoring/issues/40)) +- Possibilité de renseigner le JDD à chaque visite + ([#30](https://github.com/PnX-SI/gn_module_monitoring/issues/30)) +- Possibilité pour les administrateurs d'associer les JDD à un + sous-module directement depuis l'accueil du sous-module + ([#30](https://github.com/PnX-SI/gn_module_monitoring/issues/30)) +- Possibilité de créer des groupes de sites (encore un peu jeune) + ([#24](https://github.com/PnX-SI/gn_module_monitoring/issues/24)) +- Possibilité de créer une visite directement après la création d'un + site, et d'une observation directement après la création d'une + visite + ([#28](https://github.com/PnX-SI/gn_module_monitoring/issues/28)) +- Redirection sur sa page de détail après la création d'un objet, + plutôt que sur la liste + ([#22](https://github.com/PnX-SI/gn_module_monitoring/issues/22)) +- Mise à jour du composant de gestion et d'affichage des médias +- Ajout d'un composant de liste modulable (`datalist`) pouvant + interroger une API, pouvant être utilisé pour les listes de taxons, + d'observateurs, de jdd, de nomenclatures, de sites, de groupes de + sites, etc... + ([#44](https://github.com/PnX-SI/gn_module_monitoring/issues/44)) +- Liste des observations : ajout d'un paramètre permettant + d'afficher le nom latin des taxons observés + ([#36](https://github.com/PnX-SI/gn_module_monitoring/issues/36)) +- Simplification de la procédure pour mettre les données dans la + synthèse (un fichier à copier, un bouton à cocher et possibilité de + customiser la vue pour un sous-module) +- Passage de la complexité des méthodes de mise en base des données et + de gestion des relation par liste d'`id` (observateurs, jdd du + module, correlations site module) vers le module + [Utils\_Flask\_SQLA]{.title-ref} (amélioration de la méthode + `from_dict` en mode récursif qui accepte des listes d'`id` et les + traduit en liste de modèles), (principalement dans + `backend/monitoring/serializer.py`) +- Suppression du fichier `custom.json` pour gérer son contenu dans les + nouveaux champs de la table `gn_monitoring.t_module_complements` + ([#43](https://github.com/PnX-SI/gn_module_monitoring/issues/43)) +- Clarification et remplacement des `module_path` et `module_code` + ([#40](https://github.com/PnX-SI/gn_module_monitoring/issues/40)) **🐛 Corrections** -* Amélioration des modèles SLQA pour optimiser la partie sérialisation - ([#46](https://github.com/PnX-SI/gn_module_monitoring/issues/46)) -* Renseignement de la table `gn_synthese.t_sources` à l'installation - ([#33](https://github.com/PnX-SI/gn_module_monitoring/issues/33)) -* Passage du commentaire de la visite en correspondance avec le champs - `comment_context` de la Synthèse, dans la vue - `gn_monitoring.vs_visits` - ([#31](https://github.com/PnX-SI/gn_module_monitoring/issues/31)) -* Remplissage de la table `gn_commons.bib_tables_location` pour les - tables du schéma `gn_monitoring` si cela n'a pas été fait par - GeoNature - ([#27](https://github.com/PnX-SI/gn_module_monitoring/issues/27)) -* Corrections et optimisations diverses du code et de l'ergonomie -* Corrections de la documentation et docstrings (par \@jbdesbas) +- Amélioration des modèles SLQA pour optimiser la partie sérialisation + ([#46](https://github.com/PnX-SI/gn_module_monitoring/issues/46)) +- Renseignement de la table `gn_synthese.t_sources` à l'installation + ([#33](https://github.com/PnX-SI/gn_module_monitoring/issues/33)) +- Passage du commentaire de la visite en correspondance avec le champs + `comment_context` de la Synthèse, dans la vue + `gn_monitoring.vs_visits` + ([#31](https://github.com/PnX-SI/gn_module_monitoring/issues/31)) +- Remplissage de la table `gn_commons.bib_tables_location` pour les + tables du schéma `gn_monitoring` si cela n'a pas été fait par + GeoNature + ([#27](https://github.com/PnX-SI/gn_module_monitoring/issues/27)) +- Corrections et optimisations diverses du code et de l'ergonomie +- Corrections de la documentation et docstrings (par \@jbdesbas) **⚠️ Notes de version** Si vous mettez à jour le module depuis la version 0.1.0 : -* Les fichiers `custom.json` ne sont plus utiles (la configuration - spécifique à une installation (liste utilisateurs, etc..) est - désormais gérée dans la base de données, dans la table - `gn_monitoring.t_module_complements`) -* Dans les fichiers `config.json`, la variable `data` (pour précharger - les données (nomenclatures, etc..)) est désormais calculée depuis la - configuration. -* Pour mettre à jour la base de données, il faut exécuter le fichier - `data/migration/migration_0.1.0_0.2.0.sql` -* Suivez la procédure classique de mise à jour du module - (`docs/MAJ.rst`) - -0.1.0 (2020-06-30) -================== +- Les fichiers `custom.json` ne sont plus utiles (la configuration + spécifique à une installation (liste utilisateurs, etc..) est + désormais gérée dans la base de données, dans la table + `gn_monitoring.t_module_complements`) +- Dans les fichiers `config.json`, la variable `data` (pour précharger + les données (nomenclatures, etc..)) est désormais calculée depuis la + configuration. +- Pour mettre à jour la base de données, il faut exécuter le fichier + `data/migration/migration_0.1.0_0.2.0.sql` +- Suivez la procédure classique de mise à jour du module + (`docs/MAJ.rst`) + +## 0.1.0 (2020-06-30) Première version fonctionelle du module Monitoring de GeoNature. Nécessite la version 2.4.1 de GeoNature minimum. **Fonctionnalités** -* Génération dynamique de sous-modules de gestion de protocoles de - suivi -* Saisie et consultation de sites, visites et observations dans chaque - sous-module -* Génération dynamique des champs spécifiques à chaque sous-module au - niveau des sites, visites et observations (à partir de - configurations json et basé sur le composant `DynamicForm` de - GeoNature) -* Ajout de tables complémentaires pour étendre les tables - `t_base_sites` et `t_base_visits` du schema `gn_monitoring` - permettant de stocker dans un champs de type `jsonb` les contenus - des champs dynamiques spécifiques à chaque sous-module -* Ajout de médias locaux ou distants (images, PDF, ...) sur les - différents objets du module, stockés dans la table verticale - `gn_commons.t_medias` -* Mise en place de fonctions SQL et de vues permettant d'alimenter la - Synthèse de GeoNature à partir des données des sous-modules des - protocoles de suivi (#14) -* Ajout d'une commande d'installation d'un sous-module - (`flask monitoring install `) -* Ajout d'une commande de suppression d'un sous-module - (`remove_monitoring_module_cmd(module_code)`) -* Documentation de l'installation et de la configuration d'un - sous-module de protocole de suivi -* Des exemples de sous-modules sont présents - [ici]() +- Génération dynamique de sous-modules de gestion de protocoles de + suivi +- Saisie et consultation de sites, visites et observations dans chaque + sous-module +- Génération dynamique des champs spécifiques à chaque sous-module au + niveau des sites, visites et observations (à partir de + configurations json et basé sur le composant `DynamicForm` de + GeoNature) +- Ajout de tables complémentaires pour étendre les tables + `t_base_sites` et `t_base_visits` du schema `gn_monitoring` + permettant de stocker dans un champs de type `jsonb` les contenus + des champs dynamiques spécifiques à chaque sous-module +- Ajout de médias locaux ou distants (images, PDF, ...) sur les + différents objets du module, stockés dans la table verticale + `gn_commons.t_medias` +- Mise en place de fonctions SQL et de vues permettant d'alimenter la + Synthèse de GeoNature à partir des données des sous-modules des + protocoles de suivi (#14) +- Ajout d'une commande d'installation d'un sous-module + (`flask monitoring install `) +- Ajout d'une commande de suppression d'un sous-module + (`remove_monitoring_module_cmd(module_code)`) +- Documentation de l'installation et de la configuration d'un + sous-module de protocole de suivi +- Des exemples de sous-modules sont présents + [ici](https://github.com/PnCevennes/protocoles_suivi/) diff --git a/docs/commandes.md b/docs/commandes.md index e2c216de4..15215260c 100644 --- a/docs/commandes.md +++ b/docs/commandes.md @@ -5,7 +5,7 @@ geonature monitorings install ``` -# Mettre à jour la nommenclature +# Mettre à jour les nomenclatures Ajoute ou met à jour des nomenclatures en base de données à partir du fichier `nomenclature.json` de la config du module (voir le fichier @@ -35,7 +35,7 @@ données associées. geonature monitorings remove ``` -# Mettre à jour la synthese +# Mettre à jour la synthèse Cette commande lit la vue de synchronisation liée au module et synchronise les données dans la synthèse (insertion et mise à jour diff --git a/docs/documentation_technique.md b/docs/documentation_technique.md index 10a69807f..a56894190 100644 --- a/docs/documentation_technique.md +++ b/docs/documentation_technique.md @@ -5,3 +5,16 @@ ## Backend ## Frontend + +Une github action vérifiant le lint du frontend est réalisée . +En mode développement il est nécessaire d'effectuer avant chaque PR les commandes suivantes : + +```sh +cd frontend # se placer dans le dossier frontend +nvm use # sourcer la bonne version de node +npm run format:check # vérifier l'état des fichiers frontend +``` + +La sortie de la commande `npm run format:check` va renvoyer des warning en fonction des fichiers non lintés . + +Si c'est le cas , alors lancer la commande : `npm run format`. diff --git a/docs/images/2023-10-MCD_schema_monitoring.png b/docs/images/2023-10-MCD_schema_monitoring.png new file mode 100644 index 000000000..e235bbfe0 Binary files /dev/null and b/docs/images/2023-10-MCD_schema_monitoring.png differ diff --git a/docs/images/admin_type_site.png b/docs/images/admin_type_site.png new file mode 100644 index 000000000..440e57358 Binary files /dev/null and b/docs/images/admin_type_site.png differ diff --git a/docs/images/ajout_visite_via_site.png b/docs/images/ajout_visite_via_site.png new file mode 100644 index 000000000..c3f5037fb Binary files /dev/null and b/docs/images/ajout_visite_via_site.png differ diff --git a/docs/images/formulaire_site_type_site_champs.png b/docs/images/formulaire_site_type_site_champs.png new file mode 100644 index 000000000..5e1f16739 Binary files /dev/null and b/docs/images/formulaire_site_type_site_champs.png differ diff --git a/docs/images/page_accueil_monitoring_acces_sites.png b/docs/images/page_accueil_monitoring_acces_sites.png new file mode 100644 index 000000000..9c57ac3e8 Binary files /dev/null and b/docs/images/page_accueil_monitoring_acces_sites.png differ diff --git a/docs/images/type_site_module_config_admin_panel.png b/docs/images/type_site_module_config_admin_panel.png new file mode 100644 index 000000000..48018bfc0 Binary files /dev/null and b/docs/images/type_site_module_config_admin_panel.png differ diff --git a/docs/images/type_site_module_config_admin_panel_config.png b/docs/images/type_site_module_config_admin_panel_config.png new file mode 100644 index 000000000..6f64b2eb1 Binary files /dev/null and b/docs/images/type_site_module_config_admin_panel_config.png differ diff --git a/docs/images/type_site_module_config_front.png b/docs/images/type_site_module_config_front.png new file mode 100644 index 000000000..a26972931 Binary files /dev/null and b/docs/images/type_site_module_config_front.png differ diff --git a/docs/sous_module.md b/docs/sous_module.md index e30143f46..2034cf0d1 100644 --- a/docs/sous_module.md +++ b/docs/sous_module.md @@ -36,6 +36,31 @@ title: 'Création d''un sous-module' * `nomenclature.json` (pour l'ajout de nomenclatures spécifiques au sous-module) * `synthese.sql` (vue pour la synchronisation avec la synthèse) voir +S'ajoute à ces fichiers, des fichiers de config de `types de site` que l'on devra associer aux sous modules installés. + +Pour cela , il faut créer les types de sites via l'interface administrateur (voir les deux imags ci dessous) . + +
Images représentant l'interface administrateur au niveau du menu "Types de site" + +![Menu dans interface admin pour les types de site](docs/../images/type_site_module_config_admin_panel.png) + + +![Config dans interface admin pour les types de site](docs/../images/type_site_module_config_admin_panel_config.png) + +
+ +Ces types de site , une fois créés pourront être associés au sous module dans la configuration du module. + +
Image représentant la configuration du module avec l'association aux types de sites + +![Association des types de site au sous module](docs/../images/type_site_module_config_front.png) + +
+ + + + + ## Les exports * `exports` @@ -182,6 +207,7 @@ Pour cela il faut utiliser les variables suivantes : * `__MODULE.ID_MODULE` * `__MODULE.ID_LIST_OBSERVER` * `__MODULE.TAXONOMY_DISPLAY_FIELD_NAME` +* `__MODULE.TYPES_SITE` qui peuvent servir dans la définition des formulaires (en particulier pour les datalist). Voir ci dessous @@ -254,17 +280,24 @@ nommée `specific` dans les fichiers `site.json`, `visit.json` ou } ``` -* **utilisateur** : choix de plusieurs noms d'utilisateurs dans une - liste +* **utilisateur** : Il est possible de choisir des observateurs de deux manières différentes. Soit les observateurs sont issus d'une liste d'observateurs (voir `observers` ci dessous) soit on choisit de renseigner textuellement une liste d'observateurs (avec le champ `observers_txt` ) ```json "observers": { "attribut_label": "Observateurs", "type_widget": "observers", "type_util": "user", "code_list": "__MODULE.ID_LIST_OBSERVER", + "hidden": false, + "required": true +}, +"observers_txt": { + "hidden": true, + "required": false }, ``` +Par défaut c'est la liste d'observateurs lié au sous module qui est choisi. Si l'on veut plutôt renseigner des observateurs textuellement il suffit d'inverser les champs `hidden` et `required` entre `observers` et `observers_txt`. + Il est important d'ajouter `"type_util": "user"`. * **nomenclature** : un choix obligatoire parmi une liste définie par diff --git a/docs/synthese.md b/docs/synthese.md index d079ac6ac..894d296e2 100644 --- a/docs/synthese.md +++ b/docs/synthese.md @@ -7,7 +7,7 @@ La vue pour la synthèse * Copier le fichier `data/synthese_svo.sql` dans `/synthese.sql`. * Ce script SQL sera exécuté automatiquement à l\'installation du - module (utiliser la commande `geonature monitorings process_csv` pour le jouer + module (utiliser la commande `geonature monitorings process_sql` pour le jouer à la demande). * Cette vue peut être personalisée pour chaque module, on peut notamment : @@ -39,12 +39,12 @@ observation. de créer le lien dans la synthèse vers la module monitoring qui a généré la ligne de la synthèse) -## Mettre à jour les vues pour la synthèse +## Mettre à jour les vues pour la synthèse et des exports -Tous les fichiers de vue pour la synthèse peuvent être re-exécutés avec la commande : +Tous les fichiers de vue pour la synthèse et les exports peuvent être re-exécutés avec la commande : -- `geonature monitorings process_csv` (tous les modules) -- `geonature monitorings process_csv ` (un seul module) +- `geonature monitorings process_sql` (tous les modules) +- `geonature monitorings process_sql ` (un seul module) ## Mettre à jour la synthèse après une intégration massive de données diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 000000000..112a2eaed --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +lts/gallium \ No newline at end of file diff --git a/frontend/.prettierrc b/frontend/.prettierrc index 6365e3241..21f11dea9 100644 --- a/frontend/.prettierrc +++ b/frontend/.prettierrc @@ -5,5 +5,5 @@ "tabWidth": 2, "semi": true, "bracketSpacing": true, - "trailingComma" : "es5" + "trailingComma": "es5" } diff --git a/frontend/app/class/breadCrumb.ts b/frontend/app/class/breadCrumb.ts new file mode 100644 index 000000000..240bfee52 --- /dev/null +++ b/frontend/app/class/breadCrumb.ts @@ -0,0 +1,28 @@ +const getElementSiteGroupBC = { + description: 'Liste des groupes de site', + label: '', + url: 'object/generic/sites_group', +}; + +const getElementSiteBC = { + description: 'Liste des sites', + label: '', + url: 'object/generic/site', +}; + +export class breadCrumbBase { + static readonly baseBreadCrumbSiteGroups = new breadCrumbBase( + 'baseBreadCrumbSiteGroups', + getElementSiteGroupBC + ); + static readonly baseBreadCrumbSites = new breadCrumbBase('baseBreadCrumbSites', getElementSiteBC); + // private to disallow creating other instances of this type + private constructor( + private readonly key: string, + public readonly value: any + ) {} + + toString() { + return this.key; + } +} diff --git a/frontend/app/class/monitoring-geom-component.ts b/frontend/app/class/monitoring-geom-component.ts new file mode 100644 index 000000000..cabf070cd --- /dev/null +++ b/frontend/app/class/monitoring-geom-component.ts @@ -0,0 +1,30 @@ +import { PageInfo } from '../interfaces/page'; +import { JsonData } from '../types/jsondata'; + +const LIMIT = 10; + +type callbackFunction = (pageNumber: number, filters: JsonData, tabObj: string) => void; + +export class MonitoringGeomComponent { + protected getAllItemsCallback: callbackFunction; + protected limit = LIMIT; + public filters = {}; + public baseFilters = {}; + + constructor() {} + + setPage({ page, tabObj = '' }) { + this.getAllItemsCallback(page.offset + 1, this.filters, tabObj); + } + + setSort({ filters, tabObj = '' }) { + this.filters = { ...this.baseFilters, ...filters }; + const pageNumber = 1; + this.getAllItemsCallback(pageNumber, this.filters, tabObj); + } + + setFilter({ filters, tabObj = '' }) { + this.filters = { ...this.baseFilters, ...filters }; + this.getAllItemsCallback(1, this.filters, tabObj); + } +} diff --git a/frontend/app/class/monitoring-object-base.ts b/frontend/app/class/monitoring-object-base.ts index b40b2b2c6..0906d2d12 100644 --- a/frontend/app/class/monitoring-object-base.ts +++ b/frontend/app/class/monitoring-object-base.ts @@ -1,18 +1,15 @@ -import { Observable, of } from 'rxjs'; -import { concatMap } from 'rxjs/operators'; -import { threadId } from 'worker_threads'; -import { forkJoin } from 'rxjs'; import { MonitoringObjectService } from '../services/monitoring-object.service'; import { Utils } from '../utils/utils'; - +import { Observable, of } from 'rxjs'; +import { forkJoin } from 'rxjs'; +import { concatMap } from 'rxjs/operators'; export class MonitoringObjectBase { moduleCode: string; objectType: string; id: number; // id de l'objet - + cruved: Object; parentsPath = []; - - userCruved; + is_geom_from_child: boolean; userCruvedObject; deleted = false; @@ -33,6 +30,7 @@ export class MonitoringObjectBase { siteId; template = {}; + template_specific = {}; // configParams = ["geometry_type", "chained"]; config = {}; @@ -123,14 +121,16 @@ export class MonitoringObjectBase { } setData(data) { - this.userCruved = data.cruved; this.userCruvedObject = data.cruved_objects; this.properties = data.properties || {}; this.geometry = data.geometry; this.id = this.id || (this.properties && this.properties[this.configParam('id_field_name')]); this.medias = data.medias; - this.siteId = data.site_id; + if (data.site_id) { + this.siteId = data.site_id; + } this.idTableLocation = data.id_table_location; + this.cruved = data.cruved; } idFieldName() { @@ -171,6 +171,10 @@ export class MonitoringObjectBase { setResolvedProperties(): Observable { const observables = {}; const schema = this.schema(); + + if (Object.keys(this.template_specific).length > 0) { + Object.assign(schema, this.template_specific['schema']); + } for (const attribut_name of Object.keys(schema)) { observables[attribut_name] = this.resolveProperty( schema[attribut_name], @@ -193,11 +197,6 @@ export class MonitoringObjectBase { .configModuleObjectParam(this.moduleCode, this.objectType, fieldName); } - cruved(c = null) { - const cruved = this.configParam('cruved') || {}; - return c ? (![undefined, null].includes(cruved[c]) ? cruved[c] : 1) : cruved; - } - childrenTypes(configParam: string = null): Array { let childrenTypes = this.configParam('children_types') || []; @@ -362,22 +361,36 @@ export class MonitoringObjectBase { /** navigation */ - navigateToAddChildren(childrenType = null, id = null) { + navigateToAddChildren(childrenType = null, id = null, siteId = null) { const queryParamsAddChildren = {}; queryParamsAddChildren[this.idFieldName()] = this.id || id; + queryParamsAddChildren['siteId'] = siteId || this.siteId; queryParamsAddChildren['parents_path'] = this.parentsPath.concat(this.objectType); - this._objService.navigate( - 'create_object', - this.moduleCode, - childrenType || this.uniqueChildrenType(), - null, - queryParamsAddChildren - ); + + if (this.moduleCode == 'generic') { + this._objService.navigateGeneric( + 'object', + this.moduleCode, + childrenType || this.uniqueChildrenType(), + null, + 'create', + queryParamsAddChildren + ); + } else { + this._objService.navigate( + 'create_object', + this.moduleCode, + childrenType || this.uniqueChildrenType(), + null, + queryParamsAddChildren + ); + } } - navigateToDetail(id = null) { + navigateToDetail(id = null, toEdit = false) { this._objService.navigate('object', this.moduleCode, this.objectType, id || this.id, { parents_path: this.parentsPath, + edit: toEdit, }); } @@ -385,7 +398,6 @@ export class MonitoringObjectBase { // cas module if (this.objectType.includes('module')) { this.navigateToDetail(); - // autres cas } else { const parentType = this.parentType(); diff --git a/frontend/app/class/monitoring-object.ts b/frontend/app/class/monitoring-object.ts index 93a48a91e..976083a20 100644 --- a/frontend/app/class/monitoring-object.ts +++ b/frontend/app/class/monitoring-object.ts @@ -1,10 +1,8 @@ -import { Observable, of, forkJoin } from 'rxjs'; -import { mergeMap, concatMap } from 'rxjs/operators'; - import { MonitoringObjectService } from '../services/monitoring-object.service'; import { Utils } from '../utils/utils'; - import { MonitoringObjectBase } from './monitoring-object-base'; +import { Observable, of, forkJoin } from 'rxjs'; +import { mergeMap, concatMap } from 'rxjs/operators'; export class MonitoringObject extends MonitoringObjectBase { myClass = MonitoringObject; @@ -71,27 +69,15 @@ export class MonitoringObject extends MonitoringObjectBase { } /** Methodes get post patch delete */ - get(depth): Observable { - let bFromCache = false; - return of(true).pipe( - mergeMap(() => { - const postData = this._objService.getFromCache(this); - if (postData) { - bFromCache = true; - return of(postData); - } - return this._objService - .dataMonitoringObjectService() - .getObject(this.moduleCode, this.objectType, this.id, depth); - }), - mergeMap((postData) => { - if (!bFromCache) { - this._objService.setCache(this, postData); - } - return this.init(postData); - }) - ); + return this._objService + .dataMonitoringObjectService() + .getObject(this.moduleCode, this.objectType, this.id, depth) + .pipe( + mergeMap((postData) => { + return this.init(postData); + }) + ); } post(formValue): Observable { @@ -159,7 +145,7 @@ export class MonitoringObject extends MonitoringObjectBase { let parentOut = null; - if (!parentType || !this.parentId(parentType)) { + if (parentType != 'module' && !this.parentId(parentType)) { return of(null); } @@ -192,34 +178,6 @@ export class MonitoringObject extends MonitoringObjectBase { ); } - /** Formulaires */ - - /** formValues: obj -> from */ - - formValues(): Observable { - const properties = Utils.copy(this.properties); - const observables = {}; - const schema = this.schema(); - for (const attribut_name of Object.keys(schema)) { - const elem = schema[attribut_name]; - if (!elem.type_widget) { - continue; - } - observables[attribut_name] = this._objService.toForm(elem, properties[attribut_name]); - } - - return forkJoin(observables).pipe( - concatMap((formValues_in) => { - const formValues = Utils.copy(formValues_in); - // geometry - if (this.config['geometry_type']) { - formValues['geometry'] = this.geometry; // copy??? - } - return of(formValues); - }) - ); - } - /** postData: obj -> from */ postData(formValue) { @@ -232,11 +190,31 @@ export class MonitoringObject extends MonitoringObjectBase { } propertiesData[attribut_name] = this._objService.fromForm(elem, formValue[attribut_name]); } + // On récupère les champs spécifiques qui ne sont ni dans la config spécifique, générique ou des types de sites sélectionnés + // Permet de garder les propriétés du site sur un autre protocole qui appelle ce site avec d'autres types de sites associés + if ('additional_data_keys' in formValue && formValue['additional_data_keys'].length > 0) { + for (const key of formValue['additional_data_keys']) { + propertiesData[key] = formValue[key]; + } + } - const postData = { + let postData = {}; + postData = { properties: propertiesData, // id_parent: this.parentId }; + // if (Object.keys(dataComplement).length == 0) { + // postData = { + // properties: propertiesData, + // // id_parent: this.parentId + // }; + // } else { + // postData = { + // properties: propertiesData, + // dataComplement: dataComplement, + // // id_parent: this.parentId + // }; + // } if (this.config['geometry_type']) { postData['geometry'] = formValue['geometry']; @@ -291,6 +269,7 @@ export class MonitoringObject extends MonitoringObjectBase { (fieldName) => child.resolvedProperties[fieldName] ); row['id'] = child.id; + row['cruved'] = child.cruved; return row; }); } diff --git a/frontend/app/class/monitoring-visit.ts b/frontend/app/class/monitoring-visit.ts new file mode 100644 index 000000000..5f6c5b4fa --- /dev/null +++ b/frontend/app/class/monitoring-visit.ts @@ -0,0 +1,6 @@ +export enum columnNameVisit { + id_module = 'Protocol ID', + visit_date_max = 'Date max', + visit_date_min = 'Date min', + nb_observations = 'Nb. observations', +} diff --git a/frontend/app/components/breadcrumbs/breadcrumbs.component.css b/frontend/app/components/breadcrumbs/breadcrumbs.component.css index bda26d020..db4e0b17a 100644 --- a/frontend/app/components/breadcrumbs/breadcrumbs.component.css +++ b/frontend/app/components/breadcrumbs/breadcrumbs.component.css @@ -8,8 +8,8 @@ .breadcrumbs .link { cursor: pointer; color: rgb(0, 123, 255); -} +} .breadcrumbs .link:hover { text-decoration: underline; -} +} diff --git a/frontend/app/components/breadcrumbs/breadcrumbs.component.html b/frontend/app/components/breadcrumbs/breadcrumbs.component.html index 3afdfa835..72cb2d4cd 100644 --- a/frontend/app/components/breadcrumbs/breadcrumbs.component.html +++ b/frontend/app/components/breadcrumbs/breadcrumbs.component.html @@ -5,7 +5,8 @@ / - {{ elem.label }} : {{ elem.description }} + {{ elem.label }} : {{ elem.description + }} / diff --git a/frontend/app/components/breadcrumbs/breadcrumbs.component.ts b/frontend/app/components/breadcrumbs/breadcrumbs.component.ts index 0465da827..f04da177c 100644 --- a/frontend/app/components/breadcrumbs/breadcrumbs.component.ts +++ b/frontend/app/components/breadcrumbs/breadcrumbs.component.ts @@ -9,6 +9,12 @@ import { ConfigService } from '../../services/config.service'; import { MonitoringObject } from '../../class/monitoring-object'; import { Router } from '@angular/router'; import { ActivatedRoute } from '@angular/router'; +import { ObjectService } from '../../services/object.service'; +import { SiteSiteGroup } from '../../interfaces/objObs'; +import { IBreadCrumb } from '../../interfaces/object'; +import { breadCrumbBase } from '../../class/breadCrumb'; + +export const breadCrumbElementBase: IBreadCrumb = breadCrumbBase.baseBreadCrumbSiteGroups.value; @Component({ selector: 'pnx-monitoring-breadcrumbs', @@ -16,24 +22,32 @@ import { ActivatedRoute } from '@angular/router'; styleUrls: ['./breadcrumbs.component.css'], }) export class BreadcrumbsComponent implements OnInit { - public breadcrumbs; - + public breadcrumbs: IBreadCrumb[] = []; @Input() bEdit: boolean; @Output() bEditChange = new EventEmitter(); public frontendModuleMonitoringUrl: string; - + public newLabel: string; + public new_desc: string; @Input() obj: MonitoringObject; + // Specific to the site access + siteSiteGroup: SiteSiteGroup | null = null; constructor( private _dataMonitoringObjectService: DataMonitoringObjectService, private _configService: ConfigService, private _router: Router, - private _route: ActivatedRoute + private _route: ActivatedRoute, + private _objectService: ObjectService ) {} ngOnInit() { - // this.initBreadcrumbs(); + if (this.obj === undefined) { + this._objectService.currentDataBreadCrumb.subscribe((breadCrumb) => { + this.breadcrumbs = breadCrumb; + }); + return; + } } initBreadcrumbs() { @@ -68,18 +82,23 @@ export class BreadcrumbsComponent implements OnInit { this.bEditChange.emit(false); setTimeout(() => { if (elem) { - this._router.navigate( - [ - this._configService.frontendModuleMonitoringUrl(), - 'object', - elem.module_code, - elem.object_type, - elem.id, - ], - { - queryParams: elem.params, - } - ); + if (this.obj == undefined) { + const url = [this._configService.frontendModuleMonitoringUrl(), elem.url].join('/'); + this._router.navigateByUrl(url); + } else { + this._router.navigate( + [ + this._configService.frontendModuleMonitoringUrl(), + 'object', + elem.module_code, + elem.object_type, + elem.id, + ], + { + queryParams: elem.params, + } + ); + } } else { this._router.navigate([this._configService.frontendModuleMonitoringUrl()]); } diff --git a/frontend/app/components/btn-select/btn-select.component.css b/frontend/app/components/btn-select/btn-select.component.css new file mode 100644 index 000000000..025878545 --- /dev/null +++ b/frontend/app/components/btn-select/btn-select.component.css @@ -0,0 +1,5 @@ +.example-chip-list { + width: 100%; + background-color: white; + border-radius: 5px; +} diff --git a/frontend/app/components/btn-select/btn-select.component.html b/frontend/app/components/btn-select/btn-select.component.html new file mode 100644 index 000000000..ad3106bc2 --- /dev/null +++ b/frontend/app/components/btn-select/btn-select.component.html @@ -0,0 +1,34 @@ + + {{ titleBtn }} + + + {{ optionSelected }} + + + + + + + + {{ option.name }} + + + + + diff --git a/frontend/app/components/btn-select/btn-select.component.ts b/frontend/app/components/btn-select/btn-select.component.ts new file mode 100644 index 000000000..9283433c6 --- /dev/null +++ b/frontend/app/components/btn-select/btn-select.component.ts @@ -0,0 +1,145 @@ +import { COMMA, ENTER } from '@angular/cdk/keycodes'; +import { + Component, + ElementRef, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, +} from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { Observable, iif, of } from 'rxjs'; +import { debounceTime, distinctUntilChanged, map, startWith, concatMap } from 'rxjs/operators'; + +import { JsonData } from '../../types/jsondata'; +import { FormService } from '../../services/form.service'; + +export interface EmptyObject { + name: string; +} + +@Component({ + selector: 'btn-select', + templateUrl: './btn-select.component.html', + styleUrls: ['./btn-select.component.css'], +}) +export class BtnSelectComponent implements OnInit { + selectable = true; + removable = true; + isInit = false; + separatorKeysCodes: number[] = [ENTER, COMMA]; + myControl = new FormControl(); + listOpNeeded = new FormControl([], [Validators.required, Validators.minLength(1)]); + @Input() placeholderText: string = 'Selectionnez vos options dans la liste'; + @Input() titleBtn: string = 'Choix des options'; + + filteredOptions: Observable; + listOptionChosen: string[] = []; + configObjAdded: JsonData = {}; + genericResponse: JsonData = {}; + objToEdit: JsonData; + + @Input() bEdit: boolean; + @Input() isInitialValues: boolean; + @Input() paramToFilt: string; + @Input() callBackFunction: ( + pageNumber: number, + limit: number, + valueToFilter: string + ) => Observable; + @Input() initValueFunction: () => JsonData; + @ViewChild('optionInput') optionInput: ElementRef; + + @Output() public sendobject = new EventEmitter(); + + constructor(private _formService: FormService) {} + + ngOnInit() { + if (this.isInitialValues && !this.isInit) { + this.initFromExistingObj(this.paramToFilt); + this.objToEdit.map((val) => this.addObject(val)); + this.isInit = true; + } + this.filteredOptions = this.myControl.valueChanges.pipe( + startWith(''), + debounceTime(400), + distinctUntilChanged(), + concatMap((val: string) => { + return this.filterOnRequest(val, this.paramToFilt); + }), + map((res) => (res.length > 0 ? res : [{ name: 'Pas de résultats' }])) + ); + this.listOpNeeded.setValue(this.listOptionChosen); + this._formService.changeExtraFormControl(this.listOpNeeded, 'listOptBtnSelect'); + } + + remove(option: string): void { + const index = this.listOptionChosen.indexOf(option); + + if (index >= 0) { + this.listOptionChosen.splice(index, 1); + } + + if (this.configObjAdded && this.configObjAdded[option] !== undefined) { + delete this.configObjAdded[option]; + } + this.sendobject.emit(this.configObjAdded); + this.listOpNeeded.setValue(this.listOptionChosen); + } + + selected(event: MatAutocompleteSelectedEvent): void { + const shouldAddValue = this.checkBeforeAdding(event.option.viewValue); + shouldAddValue + ? this.listOptionChosen.push(event.option.viewValue) && this.addObject(event.option.value) + : null; + this.optionInput.nativeElement.value = ''; + this.myControl.setValue(''); + this.listOpNeeded.setValue(this.listOptionChosen); + } + + filterOnRequest(val: string, keyToFilt: string): Observable { + return this.callBackFunction(1, 100, val).pipe( + // Ici on map pour créer une liste d'objet contenant la valeur entré + map((response) => + response.items.filter((option) => { + return option[keyToFilt].toLowerCase().includes(val.toLowerCase()); + }) + ), + // Ici on map pour uniformiser la "key" utilisé pour afficher les options (default Key : 'name') + map((response) => + response.filter((obj) => { + Object.assign(obj, { name: obj[keyToFilt] })[keyToFilt]; + delete obj[keyToFilt]; + return obj; + }) + ) + ); + } + + checkBeforeAdding(valToAdd: string) { + const noValidInput = [null, '', 'Pas de résultats']; + if (noValidInput.includes(valToAdd) || this.listOptionChosen.includes(valToAdd)) { + return false; + } else { + return true; + } + } + + addObject(obj: JsonData) { + const { name, ...configAndId } = obj; + this.configObjAdded[name] = configAndId; + this.sendobject.emit(this.configObjAdded); + } + + initFromExistingObj(keyToFilt: string) { + const objInput = this.initValueFunction(); + this.objToEdit = objInput.filter((obj) => { + Object.assign(obj, { name: obj[keyToFilt] })[keyToFilt]; + delete obj[keyToFilt]; + return obj; + }); + this.objToEdit.map((obj) => this.listOptionChosen.push(obj.name)); + } +} diff --git a/frontend/app/components/draw-form/draw-form.component.css b/frontend/app/components/draw-form/draw-form.component.css index 56b14a974..a7682532d 100644 --- a/frontend/app/components/draw-form/draw-form.component.css +++ b/frontend/app/components/draw-form/draw-form.component.css @@ -1,4 +1,3 @@ - /* :host ::ng-deep .leaflet-draw-edit-edit { display: none; } */ diff --git a/frontend/app/components/draw-form/draw-form.component.html b/frontend/app/components/draw-form/draw-form.component.html index c21de5584..c458697ea 100644 --- a/frontend/app/components/draw-form/draw-form.component.html +++ b/frontend/app/components/draw-form/draw-form.component.html @@ -2,18 +2,17 @@ [options]="leafletDrawOptions" [zoomLevel]="1" (layerDrawed)="bindGeojsonForm($event)" - [geojson]="geojson ? geojson.geometry : null" + [geojson]="geoJsonService.currentLayer" [bZoomOnPoint]="bZoomOnPoint" [zoomLevelOnPoint]="zoomLevelOnPoint" [bEnable]="bEdit" > - - - - + + + diff --git a/frontend/app/components/draw-form/draw-form.component.spec.ts b/frontend/app/components/draw-form/draw-form.component.spec.ts index 0eb90169a..c879e1c3c 100644 --- a/frontend/app/components/draw-form/draw-form.component.spec.ts +++ b/frontend/app/components/draw-form/draw-form.component.spec.ts @@ -1,6 +1,5 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - import { DrawFormComponent } from './draw-form.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; describe('DrawFormComponent', () => { let component: DrawFormComponent; diff --git a/frontend/app/components/draw-form/draw-form.component.ts b/frontend/app/components/draw-form/draw-form.component.ts index 9c0818e53..178186967 100644 --- a/frontend/app/components/draw-form/draw-form.component.ts +++ b/frontend/app/components/draw-form/draw-form.component.ts @@ -1,8 +1,10 @@ import { Component, OnInit, Input, Output, EventEmitter, ViewEncapsulation } from '@angular/core'; -import { FormControl, FormGroup } from '@angular/forms'; - +import { FormControl } from '@angular/forms'; +import { isEqual } from 'lodash'; import { leafletDrawOptions } from './leaflet-draw.options'; import { CustomMarkerIcon } from '@geonature_common/map/marker/marker.component'; +import { FormService } from '../../services/form.service'; +import { GeoJSONService } from '../../services/geojson.service'; @Component({ selector: 'pnx-draw-form', @@ -30,7 +32,12 @@ export class DrawFormComponent implements OnInit { @Input() bEdit; - constructor() {} + @Input() geomFromProtocol: boolean = true; + + constructor( + private _formService: FormService, + public geoJsonService: GeoJSONService + ) {} ngOnInit() { // choix du type de geometrie @@ -44,33 +51,33 @@ export class DrawFormComponent implements OnInit { this.displayed = false; return; } - this.displayed = true; - switch (this.geometryType) { - case 'Point': { - this.leafletDrawOptions.draw.marker = { - icon: new CustomMarkerIcon(), - }; - break; - } - case 'Polygon': { - this.leafletDrawOptions.draw.polygon = { - allowIntersection: false, // Restricts shapes to simple polygons - drawError: { - color: '#e1e100', // Color the shape will turn when intersects - message: 'Intersection forbidden !', // Message that will show when intersect - }, - }; - break; - } - case 'LineString': { - this.leafletDrawOptions.draw.polyline = true; - break; - } - default: { - this.leafletDrawOptions.draw.marker = true; - break; - } + if (this.geometryType.includes('Point')) { + this.leafletDrawOptions.draw.marker = { + icon: new CustomMarkerIcon(), + }; + } + if (this.geometryType.includes('Polygon')) { + this.leafletDrawOptions.draw.polygon = { + allowIntersection: false, // Restricts shapes to simple polygons + drawError: { + color: '#e1e100', // Color the shape will turn when intersects + message: 'Intersection forbidden !', // Message that will show when intersect + }, + }; + } + if (this.geometryType.includes('LineString')) { + this.leafletDrawOptions.draw.polyline = true; + } + // default if not specified + if ( + !this.geometryType.includes('Point') && + !this.geometryType.includes('LineString') && + !this.geometryType.includes('Polygon') + ) { + this.leafletDrawOptions.draw.marker = { + icon: new CustomMarkerIcon(), + }; } this.leafletDrawOptions = { ...this.leafletDrawOptions }; @@ -82,11 +89,11 @@ export class DrawFormComponent implements OnInit { // init geometry from parentFormControl this.setGeojson(this.parentFormControl.value); // suivi formControl => composant - this.formValueChangeSubscription = this.parentFormControl.valueChanges.subscribe( - (geometry) => { - this.setGeojson(geometry); - } - ); + // this.formValueChangeSubscription = this.parentFormControl.valueChanges.subscribe( + // (geometry) => { + // this.setGeojson(geometry); + // } + // ); } } @@ -98,16 +105,33 @@ export class DrawFormComponent implements OnInit { // suivi composant => formControl bindGeojsonForm(geojson) { - this.geojson = geojson; - this.parentFormControl.setValue(geojson.geometry); + if (!this.parentFormControl) { + this._formService.currentFormMap.subscribe((dataForm) => { + if ('geometry' in dataForm.frmGp.controls) { + this.parentFormControl = dataForm.frmGp.controls['geometry'] as FormControl; + // this.parentFormControl.setValue(geojson.geometry); + this.manageGeometryChange(geojson.geometry); + } + }); + } else { + this.manageGeometryChange(geojson.geometry); + // this.parentFormControl.setValue(geojson.geometry); + } + } + + manageGeometryChange(geometry) { + if (!isEqual(geometry, this.parentFormControl.value)) { + this.parentFormControl.setValue(geometry); + } } ngOnChanges(changes) { if (changes.parentFormControl && changes.parentFormControl.currentValue) { this.initForm(); } - if (changes.geometryType && changes.geometryType.currentValue) { - this.initForm(); - } + // if (changes.geometryType && changes.geometryType.currentValue) { + // console.log("ICI changement draw form parentFormControl et geometryType") + // this.initForm(); + // } } } diff --git a/frontend/app/components/modal-msg/modal-msg.component.css b/frontend/app/components/modal-msg/modal-msg.component.css index a9a108880..bd31d5919 100644 --- a/frontend/app/components/modal-msg/modal-msg.component.css +++ b/frontend/app/components/modal-msg/modal-msg.component.css @@ -9,7 +9,7 @@ } .cmodal-background { - background-color: lightgray ; + background-color: lightgray; opacity: 0.5; } @@ -34,7 +34,9 @@ .hide-modal { opacity: 0; visibility: hidden; - transition: visibility 0.6s, opacity 0.5s; + transition: + visibility 0.6s, + opacity 0.5s; } .show-modal { diff --git a/frontend/app/components/modal-msg/modal-msg.component.html b/frontend/app/components/modal-msg/modal-msg.component.html index 55a358214..df19f9f77 100644 --- a/frontend/app/components/modal-msg/modal-msg.component.html +++ b/frontend/app/components/modal-msg/modal-msg.component.html @@ -1,11 +1,10 @@ -
-
-
-
-
-
- -
-
+
+
+
+
+
+ +
+
diff --git a/frontend/app/components/modal-msg/modal-msg.component.spec.ts b/frontend/app/components/modal-msg/modal-msg.component.spec.ts index 1cb48b052..b184d972f 100644 --- a/frontend/app/components/modal-msg/modal-msg.component.spec.ts +++ b/frontend/app/components/modal-msg/modal-msg.component.spec.ts @@ -1,6 +1,5 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - import { ModalMsgComponent } from './modal-msg.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; describe('ModalMsgComponent', () => { let component: ModalMsgComponent; diff --git a/frontend/app/components/modules/modules.component.css b/frontend/app/components/modules/modules.component.css index 5ce13655f..0339751f6 100644 --- a/frontend/app/components/modules/modules.component.css +++ b/frontend/app/components/modules/modules.component.css @@ -1,74 +1,44 @@ -h2 { +h1, h2 { text-align: center; } .modules { background-color: white; + min-height: 90vh; + padding: 1rem; + margin-bottom: 1rem; } .module { padding: 5px; } -.flex-container { - padding: 0; - margin: 0; - display: flex; - align-items: center; - justify-content: center; - -} - -.flex-item { - background-color: lightgray; - padding: 10px; - margin: 20px; - min-width: 377px; - line-height: 20px; - color: black; - font-weight: bold; - font-size: 2em; - text-align: center; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0px 0px 10px grey; - opacity: 0.7; - transition: opacity 0.2s, box-shadow 0.2s; -} a { - text-decoration:none; - color:initial; + text-decoration: none; + color: initial; } -.flex-item:hover { - opacity: 1; - box-shadow: 0px 0px 10px black; - transition: opacity 0.2s, box-shadow 0.2s; -} .module h2 { text-align: center; } - -.module-card:hover -{ - border: 1px solid #303030; - padding: 5px; - color:gray; - transition: 1s ease; +.module-card:hover { + transition: 0.1s ease; + background-color: rgba(121, 116, 116, 0.137); } -.module-card -{ - background-color:#71717129; - transition: 1s ease; +.card-body { + border-top: solid 1px rgb(211, 206, 206); } .card-img-top { height: 200px; width: 100%; object-fit: cover; -} \ No newline at end of file +} + +.isDisableBtn { + opacity: 90%; +} diff --git a/frontend/app/components/modules/modules.component.html b/frontend/app/components/modules/modules.component.html index 207e94f5e..5f0d9b48c 100644 --- a/frontend/app/components/modules/modules.component.html +++ b/frontend/app/components/modules/modules.component.html @@ -1,38 +1,64 @@ +
+ +

Chargement en cours

+
+ +
+
-
- -

Chargement en cours

-
- -
-
- -

Modules de suivi

- -
-
-
- - diff --git a/frontend/app/components/modules/modules.component.ts b/frontend/app/components/modules/modules.component.ts index f75d28bb6..1565d87bd 100644 --- a/frontend/app/components/modules/modules.component.ts +++ b/frontend/app/components/modules/modules.component.ts @@ -1,11 +1,16 @@ import { Utils } from './../../utils/utils'; import { Component, OnInit } from '@angular/core'; -import { mergeMap } from 'rxjs/operators'; +import { concatMap, map, mergeMap } from 'rxjs/operators'; /** services */ import { DataMonitoringObjectService } from '../../services/data-monitoring-object.service'; import { ConfigService } from '../../services/config.service'; import { get } from 'https'; +import { AuthService, User } from '@geonature/components/auth/auth.service'; +import { TPermission } from '../../types/permission'; +import { ObjectsPermissionMonitorings } from '../../enum/objectPermission'; +import { PermissionService } from '../../services/permission.service'; +import { TOOLTIPMESSAGEALERT } from '../../constants/guard'; @Component({ selector: 'pnx-monitoring-modules', @@ -13,6 +18,25 @@ import { get } from 'https'; styleUrls: ['./modules.component.css'], }) export class ModulesComponent implements OnInit { + currentUser: User; + canAccessSite: boolean = false; + currentPermission: TPermission = { + [ObjectsPermissionMonitorings.MONITORINGS_GRP_SITES]: { + canCreate: false, + canRead: false, + canUpdate: false, + canDelete: false, + }, + [ObjectsPermissionMonitorings.MONITORINGS_SITES]: { + canCreate: false, + canRead: false, + canUpdate: false, + canDelete: false, + }, + }; + + description: string; + titleModule: string; modules: Array = []; backendUrl: string; @@ -23,9 +47,13 @@ export class ModulesComponent implements OnInit { bLoading = false; + toolTipNotAllowed: string = TOOLTIPMESSAGEALERT; + constructor( + private _auth: AuthService, private _dataMonitoringObjectService: DataMonitoringObjectService, - private _configService: ConfigService + private _configService: ConfigService, + private _permissionService: PermissionService ) {} ngOnInit() { @@ -33,7 +61,19 @@ export class ModulesComponent implements OnInit { this._configService .init() .pipe( - mergeMap( + concatMap(() => + this._dataMonitoringObjectService.getCruvedMonitoring().pipe( + map((listObjectCruved: Object) => { + this._permissionService.setPermissionMonitorings(listObjectCruved); + }) + ) + ), + concatMap(() => + this._permissionService.currentPermissionObj.pipe( + map((permissionObject: TPermission) => (this.currentPermission = permissionObject)) + ) + ), + concatMap( this._dataMonitoringObjectService.getModules.bind(this._dataMonitoringObjectService) ) ) @@ -47,6 +87,17 @@ export class ModulesComponent implements OnInit { this._configService.appConfig.MEDIA_URL }/monitorings/`; this.bLoading = false; + this.description = this._configService.descriptionModule(); + this.titleModule = this._configService.titleModule(); + + this.canAccessSite = + this.currentPermission.MONITORINGS_SITES.canRead || + this.currentPermission.MONITORINGS_GRP_SITES.canRead; }); + + this.currentUser = this._auth.getCurrentUser(); + + this.currentUser['cruved'] = {}; + this.currentUser['cruved_objects'] = {}; } } diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css new file mode 100644 index 000000000..68f0c1a1a --- /dev/null +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css @@ -0,0 +1,81 @@ +.cell-link { + cursor: pointer; +} + +:host::ng-deep .datatable-body-row.active .datatable-row-group { + background-color: rgb(117, 227, 118) !important; +} + +.link:hover { + background-color: rgba(0, 0, 0, 0.2) !important; + transition: background-color 0.5; +} + +.link { + display: inline; + transition: background-color 0.5s; + border-radius: 5px; + padding: 0.5rem; +} + +.header-filter-span > input { + width: 100%; +} + +.header-sort-span { + /* width: 100%; */ + cursor: pointer; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.header-sort-span:hover { + background-color: rgb(245, 245, 245); +} + +.icon-sort { + font-size: 1.2em; + float: right; +} + +:host::ng-deep .sort-btn { + display: none !important; +} + +.custom-dt { + box-shadow: none !important; +} + +/* */ + +.object-link:hover { + color: lightblue; +} + +.btn-height { + height: 40px; +} + +.btn-float-right { + margin: 5px 0; +} + +.hide-spinner { + display: none; +} + +button:disabled { + cursor: not-allowed; + pointer-events: all !important; +} + +.isDisableBtn { + cursor: not-allowed; + text-decoration: none; +} + +.isDisableIcon { + opacity: 90%; + color: gray; +} diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html new file mode 100644 index 000000000..5a2d45e1d --- /dev/null +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html @@ -0,0 +1,220 @@ + + +

Attention

+

+ + Vous êtes sur le point de supprimer : + {{ rowSelected['name_object'] }} +
+

+ + + +
+
+
+ + + +
+
+ + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + help + + {{ column.name }} +
+
+ +
+
+
+
+
+
+
+
diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.spec.ts b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.spec.ts new file mode 100644 index 000000000..128db3e6f --- /dev/null +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.spec.ts @@ -0,0 +1,23 @@ +import { MonitoringDatatableComponent } from './monitoring-datatable-g.component'; +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; + +describe('MonitoringDatatableComponent', () => { + let component: MonitoringDatatableComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [MonitoringDatatableComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MonitoringDatatableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts new file mode 100644 index 000000000..d2f27e41b --- /dev/null +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts @@ -0,0 +1,393 @@ +import { + Component, + EventEmitter, + Input, + OnInit, + Output, + SimpleChanges, + SimpleChange, + TemplateRef, + ViewChild, +} from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { DatatableComponent } from '@swimlane/ngx-datatable'; +import { Subject, Subscription } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +import { TOOLTIPMESSAGEALERT } from '../../constants/guard'; +import { IColumn } from '../../interfaces/column'; +import { IobjObs, ObjDataType } from '../../interfaces/objObs'; +import { IPage } from '../../interfaces/page'; +import { DataTableService } from '../../services/data-table.service'; +import { ObjectService } from '../../services/object.service'; +import { Utils } from '../../utils/utils'; +import { SelectObject } from '../../interfaces/object'; +import { CommonService } from '@geonature_common/service/common.service'; +import { TPermission } from '../../types/permission'; +import { ObjectsPermissionMonitorings } from '../../enum/objectPermission'; + +interface ItemObjectTable { + id: number | null; + selected: boolean; + visible: boolean; + current: boolean; +} +type ItemsObjectTable = { [key: string]: ItemObjectTable[] }; + +@Component({ + selector: 'pnx-monitoring-datatable-g', + templateUrl: './monitoring-datatable-g.component.html', + styleUrls: ['./monitoring-datatable-g.component.css'], +}) +export class MonitoringDatatableGComponent implements OnInit { + @Input() rows; + @Input() colsname: IColumn[]; + @Input() page: IPage = { count: 0, limit: 0, page: 0 }; + @Input() obj; + @Input() dataTableObj; + @Input() dataTableArray; + + @Input() rowStatus: Array; + @Output() rowStatusChange = new EventEmitter(); + @Output() addVisitFromTable = new EventEmitter(); + @Output() saveOptionChildren = new EventEmitter(); + @Output() bEditChanged = new EventEmitter(); + @Input() currentUser; + @Input() permission: TPermission; + + @Output() onSort = new EventEmitter(); + @Output() onFilter = new EventEmitter(); + @Output() onSetPage = new EventEmitter(); + @Output() onDetailsRow = new EventEmitter(); + @Output() tabChanged = new EventEmitter(); + + @Output() onDeleteEvent = new EventEmitter(); + @Output() onEditEvent = new EventEmitter(); + @Output() onAddChildren = new EventEmitter(); + @Output() onAddObj = new EventEmitter(); + + @Input() bDeleteModalEmitter: EventEmitter; + @Input() parentPath: string; + + bDeleteModal: boolean = false; + bDeleteSpinner: boolean = false; + + private subscription: Subscription; + + private filterSubject: Subject = new Subject(); + displayFilter: boolean = false; + objectsStatus: ItemsObjectTable = {}; + + objectType: IobjObs; + columns; + row_save; + selected = []; + filters = {}; + + rowSelected; + + canCreateObj: boolean; + canCreateChild: boolean; + + toolTipNotAllowed: string = TOOLTIPMESSAGEALERT; + + @Input() activetabIndex: number = 0; + activetabType: string; + + @Output() objectsStatusChange: EventEmitter = new EventEmitter(); + + @ViewChild(DatatableComponent) table: DatatableComponent; + @ViewChild('actionsTemplate') actionsTemplate: TemplateRef; + @ViewChild('hdrTpl') hdrTpl: TemplateRef; + + constructor( + private _dataTableService: DataTableService, + private _objService: ObjectService, + private router: Router, + private _Activatedroute: ActivatedRoute, + private _commonService: CommonService + ) {} + + ngOnInit() { + this.subscribeToParentEmitter(); + this.initDatatable(); + } + subscribeToParentEmitter(): void { + if (this.bDeleteModalEmitter) { + this.subscription = this.bDeleteModalEmitter.subscribe((data: boolean) => { + this.bDeleteModal = this.bDeleteSpinner = false; + }); + } + } + + initDatatable() { + // IF prefered observable compare to ngOnChanges uncomment this: + // this._dataTableService.currentCols.subscribe(newCols => { this.columns = newCols }) + // this._objService.currentObjectType.subscribe((newObjType) => { + // this.objectType = newObjType; + // }); + + this.filters = {}; + this.filterSubject.pipe(debounceTime(500)).subscribe(() => { + this.filter(); + }); + } + + changeActiveTab(tab) { + this.activetabIndex = tab.index; + // Réinitialisation des données selectés + this.activetabType = this.dataTableArray[tab.index].objectType; + this.dataTableObj[this.activetabType].rows.length > 0 + ? (this.columns = this._dataTableService.colsTable( + this.dataTableObj[this.activetabType].columns, + this.dataTableObj[this.activetabType].rows[0] + )) + : null; + this.rows = this.dataTableObj[this.activetabType].rows; + this.page = this.dataTableObj[this.activetabType].page; + this.objectsStatusChange.emit(this.reInitStatut()); + this.tabChanged.emit(this.activetabType); + this.initPermissionAction(); + } + + reInitStatut() { + let status_type = Utils.copy(this.objectsStatus); + for (let typeObject in status_type) { + if (Array.isArray(status_type[typeObject])) { + for (let i in status_type[typeObject]) { + try { + status_type[typeObject][i]['selected'] = false; + } catch (error) { + console.error(error.message, status_type[typeObject][i]); + } + } + } + } + return status_type; + } + + displayNumber(chidrenType) { + if (!this.objectsStatus[chidrenType]) { + return ''; + } + const visibles = this.objectsStatus[chidrenType].filter((s) => s.visible); + // const nbSelected = visibles.length; + const nbSelected = this.dataTableObj[chidrenType].page.count; + const nb = this.dataTableObj[chidrenType].page.total; + return nb == nbSelected ? `(${nb})` : `(${nbSelected}/${nb})`; + } + + onSortEvent($event) { + this.filters = { + ...this.filters, + sort: $event.column.prop, + sort_dir: $event.newValue, + }; + this.onSort.emit({ filters: this.filters, tabObj: this.activetabType }); + } + + setPage($event) { + this.onSetPage.emit({ page: $event, tabObj: this.activetabType }); + } + + filterInput($event) { + this.filterSubject.next(); + } + + filter(bInitFilter = false) { + // filter all + const oldFilters = this.filters; + this.filters = Object.keys(oldFilters).reduce(function (r, e) { + if (![undefined, '', null].includes(oldFilters[e])) r[e] = oldFilters[e]; + return r; + }, {}); + this.onFilter.emit({ filters: this.filters, tabObj: this.activetabType }); + } + + onRowClick(event) { + if (!(event && event.type === 'click')) { + return; + } + switch (this.activetabType) { + case 'sites_group': + this.rowStatusChange.emit([this.activetabType, event.row.id_sites_group]); + break; + case 'site': + this.rowStatusChange.emit([this.activetabType, event.row.id_base_site]); + break; + } + } + + saveOptionChild($event: SelectObject) { + this.saveOptionChildren.emit($event); + } + + setSelected() { + // this.table._internalRows permet d'avoir les ligne triées et d'avoir les bons index + if (!this.rowStatus) { + return; + } + + const status_selected = this.rowStatus.find((status) => status.selected); + if (!status_selected) { + return; + } + + const index_row_selected = this.table._internalRows.findIndex( + (row) => row.id === status_selected.id + ); + if (index_row_selected === -1) { + return; + } + + this.selected = [this.table._internalRows[index_row_selected]]; + this.table.offset = Math.floor(index_row_selected / this.table._limit); + } + + initPermissionAction() { + let objectType: ObjectsPermissionMonitorings | string; + let objectTypeChild: ObjectsPermissionMonitorings | string; + switch (this.activetabType) { + case 'sites_group': + objectType = ObjectsPermissionMonitorings.MONITORINGS_GRP_SITES; + objectTypeChild = ObjectsPermissionMonitorings.MONITORINGS_SITES; + this.canCreateChild = this.permission[objectTypeChild].canCreate ? true : false; + break; + case 'site': + objectType = ObjectsPermissionMonitorings.MONITORINGS_SITES; + objectTypeChild = 'visit'; + this.canCreateChild = true; + break; + case 'visit': + objectType = 'visit'; + objectTypeChild = 'undefined'; + this.canCreateObj = true; + this.canCreateChild = true; + break; + default: + objectType = 'undefined'; + objectTypeChild = 'undefined'; + this.canCreateObj = false; + this.canCreateChild = false; + } + + if (!['undefined', 'visit'].includes(objectType)) { + this.canCreateObj = this.permission[objectType].canCreate ? true : false; + } + } + + ngOnDestroy() { + this.filterSubject.unsubscribe(); + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + // tooltip(column) { + // return this.child0.template.fieldDefinitions[column.prop] + // ? column.name + " : " + this.child0.template.fieldDefinitions[column.prop] + // : column.name; + // } + + ngOnChanges(changes: SimpleChanges) { + if (changes.activetabIndex) { + this.clearFilters(); + } + + if (changes.obj) { + this.updateDataTable(changes.obj); + } + + if (changes.rows || changes.page) { + this.updateRowsAndPage(); + } + + for (const propName of Object.keys(changes)) { + switch (propName) { + case 'rowStatus': + this.setSelected(); + break; + } + } + } + + private clearFilters() { + this.filters = {}; + } + + private updateDataTable(objChanges: SimpleChange) { + if (this.dataTableObj && Object.keys(this.dataTableObj).length > 0) { + for (const objType in this.dataTableObj) { + this.objectsStatus[objType] = this._dataTableService.initObjectsStatus( + this.dataTableObj[objType].rows, + objType + ); + } + + this.activetabType = this.dataTableArray[this.activetabIndex].objectType; + const dataTable = this.dataTableObj[this.activetabType]; + if (dataTable.rows.length > 0) { + this.columns = this._dataTableService.colsTable(dataTable.columns, dataTable.rows[0]); + } + this.rows = dataTable.rows; + this.page = dataTable.page; + this.initPermissionAction(); + } + } + + private updateRowsAndPage() { + if (this.rows && this.rows.length > 0) { + this.activetabType = this.dataTableArray[this.activetabIndex].objectType; + const dataTable = this.dataTableObj[this.activetabType]; + this.rows = dataTable.rows; + this.page = dataTable.page; + this.initPermissionAction(); + } + } + + addChildrenVisit(selected) { + this.addVisitFromTable.emit({ rowSelected: selected, objectType: this.activetabType }); + } + + navigateToAddChildren(_, row) { + this._objService.changeObjectType(this.dataTableArray[this.activetabIndex]); + row['object_type'] = this.dataTableArray[this.activetabIndex]['childType']; + this.onAddChildren.emit(row); + } + + navigateToAddObj() { + this._objService.changeObjectType(this.dataTableArray[this.activetabIndex]); + this.onAddObj.emit(this.dataTableArray[this.activetabIndex]['objectType']); + } + + navigateToDetail(row) { + row['id'] = row.pk; + this.onDetailsRow.emit(row); + } + + editSelectedItem(row) { + row['id'] = row.pk; + this.onEditEvent.emit(row); + } + + msgToaster(action) { + // return `${action} ${this.obj.labelDu()} ${this.obj.description()} effectuée`.trim(); + return `${action} effectuée`.trim(); + } + + onDelete(row) { + this.bDeleteSpinner = true; + row['id'] = row[row.pk]; + this._commonService.regularToaster('info', this.msgToaster('Suppression')); + this.onDeleteEvent.emit({ rowSelected: row, objectType: this.activetabType }); + } + + alertMessage(row) { + row['id'] = row[row.pk]; + this.rowSelected = row; + const varNameObjet = this.dataTableArray[this.activetabIndex].config.description_field_name; + + this.rowSelected['name_object'] = row[varNameObjet]; + this.bDeleteModal = true; + } +} diff --git a/frontend/app/components/monitoring-datatable/monitoring-datatable.component.css b/frontend/app/components/monitoring-datatable/monitoring-datatable.component.css index 6017b44ab..1e5cd3eef 100644 --- a/frontend/app/components/monitoring-datatable/monitoring-datatable.component.css +++ b/frontend/app/components/monitoring-datatable/monitoring-datatable.component.css @@ -1,48 +1,63 @@ .cell-link { - cursor: pointer; + cursor: pointer; } :host::ng-deep .datatable-body-row.active .datatable-row-group { - background-color: rgb(117, 227, 118) !important; + background-color: rgb(117, 227, 118) !important; } .link:hover { - background-color: rgba(0, 0, 0, 0.2) !important; - transition: background-color 0.5; + background-color: rgba(0, 0, 0, 0.2) !important; + transition: background-color 0.5; } .link { - display: inline; - transition: background-color 0.5s; - border-radius: 5px; + display: inline; + transition: background-color 0.5s; + border-radius: 5px; + padding: 0.5rem; } .header-filter-span > input { - width: 100%; + width: 100%; } .header-sort-span { - /* width: 100%; */ - cursor: pointer; - text-overflow: ellipsis; - overflow: hidden; - white-space:nowrap + /* width: 100%; */ + cursor: pointer; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; } - .header-sort-span:hover { - background-color: rgb(245, 245, 245); + background-color: rgb(245, 245, 245); } .icon-sort { - font-size: 1.2em; - float: right; + font-size: 1.2em; + float: right; } :host::ng-deep .sort-btn { - display: none !important; + display: none !important; } .custom-dt { - box-shadow: none !important; + box-shadow: none !important; +} + +button:disabled { + cursor: not-allowed; + pointer-events: all !important; +} + +.isDisableBtn { + cursor: not-allowed; + text-decoration: none; +} + +.isDisableIcon { + opacity: 90%; + color: gray; } diff --git a/frontend/app/components/monitoring-datatable/monitoring-datatable.component.html b/frontend/app/components/monitoring-datatable/monitoring-datatable.component.html index 40257d7dd..fe2e415b0 100644 --- a/frontend/app/components/monitoring-datatable/monitoring-datatable.component.html +++ b/frontend/app/components/monitoring-datatable/monitoring-datatable.component.html @@ -1,3 +1,21 @@ + +

Attention

+

+ Vous êtes sur le point de supprimer + {{ this.child0.template['label_art_def'] }} + {{ this.child0.template['description'] }} +

+ + +
- - + + + + + - + + + + + + + @@ -61,18 +116,19 @@
- {{column.definition}} - + {{ column.definition }} + help - {{ column.name }} + >help + {{ column.name }}
{ let component: MonitoringDatatableComponent; diff --git a/frontend/app/components/monitoring-datatable/monitoring-datatable.component.ts b/frontend/app/components/monitoring-datatable/monitoring-datatable.component.ts index 8784b6b35..43732ee86 100644 --- a/frontend/app/components/monitoring-datatable/monitoring-datatable.component.ts +++ b/frontend/app/components/monitoring-datatable/monitoring-datatable.component.ts @@ -9,10 +9,14 @@ import { SimpleChanges, TemplateRef, } from '@angular/core'; -import { Router } from '@angular/router'; import { MonitoringObjectService } from './../../services/monitoring-object.service'; +import { ListService } from '../../services/list.service'; import { Subject } from 'rxjs'; import { catchError, map, tap, take, debounceTime } from 'rxjs/operators'; +import { CommonService } from '@geonature_common/service/common.service'; +import { ObjectService } from '../../services/object.service'; +import { TOOLTIPMESSAGEALERT } from '../../constants/guard'; +import { Utils } from '../../utils/utils'; @Component({ selector: 'pnx-monitoring-datatable', @@ -24,14 +28,16 @@ export class MonitoringDatatableComponent implements OnInit { @Input() columns; @Input() sorts; - @Input() obj; @Input() child0; @Input() frontendModuleMonitoringUrl; - @Input() rowStatus: Array; @Output() rowStatusChange = new EventEmitter(); + @Input() filters: Object; + @Output() onFilter = new EventEmitter(); + @Output() onDeleteRow = new EventEmitter(); + @Output() bEditChanged = new EventEmitter(); @Input() currentUser; @@ -41,23 +47,55 @@ export class MonitoringDatatableComponent implements OnInit { row_save; selected = []; - filters = {}; customColumnComparator; @ViewChild(DatatableComponent) table: DatatableComponent; @ViewChild('actionsTemplate') actionsTemplate: TemplateRef; @ViewChild('hdrTpl') hdrTpl: TemplateRef; - constructor(private _monitoring: MonitoringObjectService) {} + rowSelected; + bDeleteModal: boolean = false; + bDeleteSpinner: boolean = false; + toolTipNotAllowed: string = TOOLTIPMESSAGEALERT; + canCreateChild: boolean = false; + canDeleteObj: boolean = false; + + constructor( + private _monitoring: MonitoringObjectService, + private _commonService: CommonService, + private _objectService: ObjectService, + private _listService: ListService + ) {} ngOnInit() { this.initDatatable(); + this.initPermission(); + } + + initPermission() { + // TODO: Attention ici l'ajout avec l'icon ne se fait que sur un enfant (si plusieurs enfants au même niveau , le premier sera pris pour le moment) + const childrenType = this.child0.config.children_types[0]; + this.canCreateChild = !!childrenType && this.currentUser?.moduleCruved[childrenType]['C']; + this.canDeleteObj = !['site', 'sites_group'].includes(this.child0.objectType); } initDatatable() { this.filters = this.child0.configParam('filters'); + + if (this._listService.arrayTableFilters$.getValue() == null) { + this._listService.arrayTableFilters = []; + } + this._listService.arrayTableFilters[this.child0.objectType] = Utils.copy(this.filters); + + // Default value + if (this._listService.listType$.getValue() == this.child0.objectType) { + this._listService.tableFilters = Utils.copy(this.filters); + } + + // Subscribe to filters event this.filterSubject.pipe(debounceTime(500)).subscribe(() => { - this.filter(); + this.filter(false); + this._listService.tableFilters = Utils.copy(this.filters); }); this.customColumnComparator = this.customColumnComparator_(); @@ -76,7 +114,6 @@ export class MonitoringDatatableComponent implements OnInit { filter(bInitFilter = false) { // filter all - let bChange = false; const temp = this.row_save.filter((row, index) => { let bCondVisible = true; @@ -91,66 +128,49 @@ export class MonitoringDatatableComponent implements OnInit { bCondVisible = bCondVisible && (String(row[key]) || '').toLowerCase().includes(v); } } + bChange = bChange || bCondVisible; - if (!this.rowStatus) { - return bCondVisible; - } - bChange = bChange || bCondVisible !== this.rowStatus[index].visible; - this.rowStatus[index]['visible'] = bCondVisible; - this.rowStatus[index]['selected'] = this.rowStatus[index]['selected'] && bCondVisible; return bCondVisible; }); - if (bChange || bInitFilter) { - this.rowStatusChange.emit(this.rowStatus); - } - // update the rows + this.rowStatusChange.emit({}); + + // Emmet les filtrers et le nombre de données répondant aux critères dans la liste + this.onFilter.emit({ filters: this.filters, nb_row: temp.length }); + this.rows = temp; // Whenever the filter changes, always go back to the first page this.table.offset = 0; - this.setSelected(); + this.selected = []; } - onRowClick(event) { - if (!(event && event.type === 'click')) { + setSelected(id) { + const status_selected = id; + + if (!status_selected) { return; } - const id = event.row && event.row.id; - - if (!this.rowStatus) { + const index_row_selected = this.table._internalRows.findIndex((row) => row.id === id); + if (index_row_selected === -1) { return; } - this.rowStatus.forEach((status) => { - const bCond = status.id === id; - status['selected'] = bCond && !status['selected']; + this.rows.forEach((row) => { + const bCond = row.id === id; + row['_internal_status_visible'] = bCond && !row['_internal_status_visible']; }); - this.setSelected(); - this.rowStatusChange.emit(this.rowStatus); + this.selected = [this.table._internalRows[index_row_selected]]; + this.table.offset = Math.floor(index_row_selected / this.table._limit); } - setSelected() { - // this.table._internalRows permet d'avoir les ligne triées et d'avoir les bons index - - if (!this.rowStatus) { - return; - } - - const status_selected = this.rowStatus.find((status) => status.selected); - if (!status_selected) { - return; - } - - const index_row_selected = this.table._internalRows.findIndex( - (row) => row.id === status_selected.id - ); - if (index_row_selected === -1) { + onRowClick(event) { + if (!(event && event.type === 'click')) { return; } - - this.selected = [this.table._internalRows[index_row_selected]]; - this.table.offset = Math.floor(index_row_selected / this.table._limit); + const id = event.row && event.row.id; + this.setSelected(event.row.id); + this.rowStatusChange.emit(event.row); } ngOnDestroy() { @@ -169,9 +189,6 @@ export class MonitoringDatatableComponent implements OnInit { const cur = chng.currentValue; const pre = chng.currentValue; switch (propName) { - case 'rowStatus': - this.setSelected(); - break; case 'child0': this.customColumnComparator = this.customColumnComparator_(); break; @@ -234,4 +251,29 @@ export class MonitoringDatatableComponent implements OnInit { return out; }; } + + msgToaster(action) { + // return `${action} ${this.obj.labelDu()} ${this.obj.description()} effectuée`.trim(); + return `${action} effectuée`.trim(); + } + + onDelete(row) { + this._monitoring + .dataMonitoringObjectService() + .deleteObject(this.obj.moduleCode, this.child0.objectType, row.id) + .subscribe(() => { + this._commonService.regularToaster('info', this.msgToaster('Suppression')); + + this.onDeleteRow.emit({ + rowSelected: row, + objectType: this.child0.objectType, + }); + this.bDeleteModal = false; + }); + } + + alertMessage(row) { + this.rowSelected = row; + this.bDeleteModal = true; + } } diff --git a/frontend/app/components/monitoring-form/monitoring-form.component.css b/frontend/app/components/monitoring-form/monitoring-form.component.css index a5c26b9ca..a0e7aeb5b 100644 --- a/frontend/app/components/monitoring-form/monitoring-form.component.css +++ b/frontend/app/components/monitoring-form/monitoring-form.component.css @@ -4,6 +4,10 @@ padding: 0; } +:host ::ng-deep .full-wrapper { + padding-bottom: 2rem!important; +} + .hide-spinner { display: none; } @@ -12,20 +16,43 @@ height: 39px; } -.float-right { -margin-left: 5px; -} - .float-left { margin-right: 10px; float: left; } + form:invalid { outline: none; } form.ng-invalid { border: 0px !important; -} \ No newline at end of file +} + +.form-scroll-info-geom { + overflow-y: auto; + max-height: 60vh; +} + +.form-scroll { + overflow-y: auto; + max-height: 70vh; +} + +.btn-child { + border: 0px solid #202020; + padding-top: 2px; + padding-bottom: auto; + -webkit-box-shadow: 0px -2px 2px rgba(50, 50, 50, 0.3); + -moz-box-shadow: 0px -2px 2px rgba(50, 50, 50, 0.3); + box-shadow: 0px -2px 2px rgba(50, 50, 50, 0.3); + overflow: hidden; + height: fit-content; +} + +button:disabled { + cursor: not-allowed; + pointer-events: all !important; +} diff --git a/frontend/app/components/monitoring-form/monitoring-form.component.html b/frontend/app/components/monitoring-form/monitoring-form.component.html index fceecbca6..c968994f4 100644 --- a/frontend/app/components/monitoring-form/monitoring-form.component.html +++ b/frontend/app/components/monitoring-form/monitoring-form.component.html @@ -2,8 +2,8 @@

Attention

Vous êtes sur le point de supprimer - {{ this.obj.template["label_art_def"] }} - {{ this.obj.template["description"] }} + {{ this.obj.template['label_art_def'] }} + {{ this.obj.template['description'] }}

- +
-
- - - +
+ - - - + + + + +
+

Veuillez saisir une géométrie sur la carte

+

+ + La géométrie de groupe site est actuellement auto-générée sur la base des sites enfants + (si existants). Vous avez la possibilité de créer vous même une géométrie + +

- - - -
+
+ + + + + +

Champs associés au type: {{obj.config['types_site'][item.key].name}}

+ +
+
+
+
+
- diff --git a/frontend/app/components/monitoring-form/monitoring-form.component.spec.ts b/frontend/app/components/monitoring-form/monitoring-form.component.spec.ts index f34b4c007..4db2a6a8d 100644 --- a/frontend/app/components/monitoring-form/monitoring-form.component.spec.ts +++ b/frontend/app/components/monitoring-form/monitoring-form.component.spec.ts @@ -1,6 +1,5 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - import { MonitoringFormComponent } from './monitoring-form.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; describe('MonitoringFormComponent', () => { let component: MonitoringFormComponent; diff --git a/frontend/app/components/monitoring-form/monitoring-form.component.ts b/frontend/app/components/monitoring-form/monitoring-form.component.ts index 3e0ae5634..1e1b8bedd 100644 --- a/frontend/app/components/monitoring-form/monitoring-form.component.ts +++ b/frontend/app/components/monitoring-form/monitoring-form.component.ts @@ -1,5 +1,12 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { + FormGroup, + FormBuilder, + Validators, + FormControl, + FormArray, + AbstractControl, +} from '@angular/forms'; import { MonitoringObject } from '../../class/monitoring-object'; // import { Router } from "@angular/router"; import { ConfigService } from '../../services/config.service'; @@ -7,6 +14,26 @@ import { DataUtilsService } from '../../services/data-utils.service'; import { CommonService } from '@geonature_common/service/common.service'; import { DynamicFormService } from '@geonature_common/form/dynamic-form-generator/dynamic-form.service'; import { ActivatedRoute } from '@angular/router'; +import { JsonData } from '../../types/jsondata'; +import { SitesService } from '../../services/api-geom.service'; +import { + concatMap, + distinctUntilChanged, + mergeMap, + switchMap, + tap, + map, + reduce, + filter, + defaultIfEmpty, + scan, +} from 'rxjs/operators'; +import { defer, forkJoin, from, iif, of, Observable } from 'rxjs'; +import { FormService } from '../../services/form.service'; +import { Router } from '@angular/router'; +import { TOOLTIPMESSAGEALERT, TOOLTIPMESSAGEALERT_CHILD } from '../../constants/guard'; +import { GeoJSONService } from '../../services/geojson.service'; +import { Utils } from '../../utils/utils'; @Component({ selector: 'pnx-monitoring-form', @@ -21,19 +48,37 @@ export class MonitoringFormComponent implements OnInit { @Input() obj: MonitoringObject; @Output() objChanged = new EventEmitter(); - @Input() objectsStatus; - @Output() objectsStatusChange = new EventEmitter(); - @Input() bEdit: boolean; @Output() bEditChange = new EventEmitter(); @Input() sites: {}; + // Possibilité d'ajouter des enfants depuis le formulaire parent + @Input() addChildren: boolean = true; + searchSite = ''; objFormsDefinition; - meta: {}; + meta: JsonData = {}; + + // objFormDynamic: FormGroup = this._formBuilder.group({}); + // objFormsDefinitionDynamic; + + objFormsDynamic: { [key: string]: FormGroup } = {}; + objFormsDefinitionDynamic: JsonData = {}; + + allTypesSiteConfig: JsonData = {}; + typesSiteConfig: JsonData = {}; + specificConfig: JsonData = {}; + confiGenericSpec: JsonData = {}; + schemaUpdate = {}; + // idsTypesSite: number[] = []; + idsTypesSite: Set = new Set(); + lastGeom = {}; + dataComplement = {}; + schemaGeneric = {}; + // confiGenericSpec = {}; public bSaveSpinner = false; public bSaveAndAddChildrenSpinner = false; @@ -45,70 +90,178 @@ export class MonitoringFormComponent implements OnInit { public queryParams = {}; + geomCalculated: boolean = false; + canDelete: boolean; + canUpdate: boolean; + toolTipNotAllowed: string = TOOLTIPMESSAGEALERT; + + isSiteObject: boolean = false; + isEditObject: boolean = false; + displayProperties: string[] = []; + hasDynamicGroups: boolean = false; + isInitialzedObjFormDynamic: { keys: string; value: boolean } | {} = {}; + remainingTypeSiteProp: JsonData = {}; + constructor( private _formBuilder: FormBuilder, private _route: ActivatedRoute, private _configService: ConfigService, private _commonService: CommonService, private _dataUtilsService: DataUtilsService, - private _dynformService: DynamicFormService + private _dynformService: DynamicFormService, + private _siteService: SitesService, + private _formService: FormService, + private _router: Router, + private _geojsonService: GeoJSONService ) {} ngOnInit() { + // Initialisation des permissions de l'utilisateur courant + this.initPermission(); + + // Récupération de la configuration du module this._configService .init(this.obj.moduleCode) - .pipe() + .pipe( + tap(() => { + // Initialisation des variables + this.initializeVariables(this.obj); + + // Initialisation des configurations + // Selon si l'objet est ou non de type site + if (this.isSiteObject) { + this.initializeTypeSiteConfig( + this.obj.config['generic'], + this.obj.config['specific'], + this.obj.config['types_site'], + this.obj['properties']['types_site'] + ); + // Filtre des types de site du module par rapport au type de site de l'objet + // Utile pour afficher les formulaires des types de sites de l'objet + // Utile pour traiter les types de site de l'objet non présents dans le module @TODO + const objFiltered = Utils.filterObject( + this.allTypesSiteConfig, + Array.from(this.idsTypesSite) + ); + this.typesSiteConfig = {}; + for (const typeSite in objFiltered) { + this.objFormsDynamic[typeSite] = this._formBuilder.group({}); + this.isInitialzedObjFormDynamic[typeSite] = true; + this.typesSiteConfig[typeSite] = this.allTypesSiteConfig[typeSite]; + } + // Initialisation des sous forms group par type de site + this._formService.addMultipleFormGroupsToObjForm(this.objFormsDynamic, this.objForm); + } else { + this.initializeSpecificConfig(this.obj.config['generic'], this.obj.config['specific']); + } + + // Initialisation des paramètres par défaut du formulaire + this.queryParams = this._route.snapshot.queryParams || {}; + + this.bChainInput = this._configService.frontendParams()['bChainInput']; + + this.meta = { + nomenclatures: this._dataUtilsService.getDataUtil('nomenclature'), + dataset: this._dataUtilsService.getDataUtil('dataset'), + id_role: this.currentUser.id_role, + bChainInput: this.bChainInput, + parents: this.obj.parents, + }; + // Récupération de la définition du formulaire + this.objFormsDefinition = this.initObjFormDefiniton(this.confiGenericSpec, this.meta); + // Tri des proprités en fonction de la variable display_properties + this.displayProperties = [...(this.obj.configParam('display_properties') || [])]; + this.objFormsDefinition = this.sortObjFormDefinition( + this.displayProperties, + this.objFormsDefinition + ); + + // Si le type d'objet est un site rajout des définitions des types de site + // a l'objet principal + if (this.isSiteObject) { + Object.entries(this.allTypesSiteConfig).forEach(([typeSite, config]) => { + let objFormDefinitonTypeSite = this.initObjFormDefiniton(config, this.meta); + // Tri des propriétés spécifiques au type de site + this.objFormsDefinitionDynamic[typeSite] = this.sortObjFormDefinition( + this.obj.config['types_site'][typeSite]['display_properties'], + objFormDefinitonTypeSite + ); + }); + } + // Ajout de controle (champ) au formulaire + // Ajout patch_update ?? TODO comprendre pourquoi + this.objForm = this._formService.addFormCtrlToObjForm( + { frmCtrl: this._formBuilder.control(0), frmName: 'patch_update' }, + this.objForm + ); + + if (this.obj.config['geometry_type']) { + const validatorRequired = + this.obj.objectType == 'sites_group' + ? this._formBuilder.control('') + : this._formBuilder.control('', Validators.required); + + let frmCtrlGeom = { + frmCtrl: validatorRequired, + frmName: 'geometry', + }; + + this.objForm = this._formService.addFormCtrlToObjForm(frmCtrlGeom, this.objForm); + } + // Conversion des query params de type entier mais en string en int + // ??? A comprendre + this.obj = this.setQueryParams(this.obj); + }), + switchMap(() => + this.initObjFormValues(this.obj, this.confiGenericSpec, Array.from(this.idsTypesSite)) + ), + switchMap((genericFormValues) => + defer(() => { + // Patch les valeurs du formulaire avec celle des propriétés spécifique aux types de site + if (this.isSiteObject) { + let siteConfig = this.allTypesSiteConfig; + if (this.isEditObject) { + siteConfig = Utils.filterObject( + this.allTypesSiteConfig, + Array.from(this.idsTypesSite) + ); + } + return this.initObjFormSpecificValues(this.obj, siteConfig).pipe( + defaultIfEmpty(null) + ); + } else { + return of(null); + } + }).pipe( + tap((specificFormValues) => { + console.log('Patching the object form values'); + this.objForm.patchValue(genericFormValues); + if (specificFormValues !== null) { + this._formService.patchValuesInDynamicGroups( + specificFormValues, + this.objFormsDynamic + ); + } + }) + ) + ) + ) .subscribe(() => { - // return this._route.queryParamMap; - // }) - // .subscribe((queryParams) => { - this.queryParams = this._route.snapshot.queryParams || {}; - this.bChainInput = this._configService.frontendParams()['bChainInput']; - const schema = this.obj.schema(); - // init objFormsDefinition - - // meta pour les parametres dynamiques - // ici pour avoir acces aux nomenclatures - this.meta = { - nomenclatures: this._dataUtilsService.getDataUtil('nomenclature'), - dataset: this._dataUtilsService.getDataUtil('dataset'), - id_role: this.currentUser.id_role, - bChainInput: this.bChainInput, - parents: this.obj.parents, - }; - this.objFormsDefinition = this._dynformService - .formDefinitionsdictToArray(schema, this.meta) - .filter((formDef) => formDef.type_widget) - .sort((a, b) => { - // medias à la fin - return a.attribut_name === 'medias' ? +1 : b.attribut_name === 'medias' ? -1 : 0; - }); - - // display_form pour customiser l'ordre dans le formulaire - // les éléments de display form sont placé en haut dans l'ordre du tableau - // tous les éléments non cachés restent affichés - let displayProperties = [...(this.obj.configParam('display_properties') || [])]; - if (displayProperties && displayProperties.length) { - displayProperties.reverse(); - this.objFormsDefinition.sort((a, b) => { - let indexA = displayProperties.findIndex((e) => e == a.attribut_name); - let indexB = displayProperties.findIndex((e) => e == b.attribut_name); - return indexB - indexA; - }); - } - - // champs patch pour simuler un changement de valeur et déclencher le recalcul des propriété - // par exemple quand bChainInput change - this.objForm.addControl('patch_update', this._formBuilder.control(0)); - - // set geometry - if (this.obj.config['geometry_type']) { - this.objForm.addControl('geometry', this._formBuilder.control('', Validators.required)); - } - - // pour donner la valeur de idParent + this.obj.bIsInitialized = true; + const dynamicGroupsArray = this.objForm.get('dynamicGroups') as FormArray; + if (dynamicGroupsArray) this.subscribeToDynamicGroupsChanges(dynamicGroupsArray); + this.setDefaultFormValue(); + }); + } - this.initForm(); + subscribeToDynamicGroupsChanges(dynamicGroupsArray: FormArray): void { + dynamicGroupsArray.valueChanges + .pipe( + scan((prevLength, currentValue) => dynamicGroupsArray.controls.length, 0), + distinctUntilChanged() + ) + .subscribe((length) => { + this.hasDynamicGroups = length > 0; }); } @@ -119,16 +272,19 @@ export class MonitoringFormComponent implements OnInit { ); } - setQueryParams() { - // par le biais des parametre query de route on donne l'id du ou des parents + setQueryParams(obj: MonitoringObject) { + // par le biais des parametres query de route on donne l'id du ou des parents // permet d'avoir un 'tree' ou un objet est sur plusieurs branches // on attend des ids d'où test avec parseInt + + // TODO COMPRENDRE Comment c'est utilisé par la suite for (const key of Object.keys(this.queryParams)) { const strToInt = parseInt(this.queryParams[key]); if (!Number.isNaN(strToInt)) { - this.obj.properties[key] = strToInt; + obj.properties[key] = strToInt; } } + return obj; } /** initialise le formulaire quand le formulaire est prêt ou l'object est prêt */ @@ -136,15 +292,41 @@ export class MonitoringFormComponent implements OnInit { if (!(this.objForm && this.obj.bIsInitialized)) { return; } + this._formService + .formValues(this.obj, this.confiGenericSpec) + .pipe( + map((genericFormValues) => { + // FIXME: renvoyer les ids des types de site coté backend et non les types de site en chaine de caractères + if ( + this.idsTypesSite.size != 0 && + genericFormValues['types_site'].every( + (item) => typeof item !== 'number' && !Number.isInteger(item) + ) + ) { + genericFormValues['types_site'] = Array.from(this.idsTypesSite); + } + return genericFormValues; + }) + ) + .subscribe((formValue) => { + this.objForm.patchValue(formValue); + this.setDefaultFormValue(); + }); + } - this.setQueryParams(); - + initFormDynamic(typeSite: string) { + if (!(this.objFormsDynamic[typeSite] && this.obj.bIsInitialized)) { + return; + } + if (this.isInitialzedObjFormDynamic && this.isInitialzedObjFormDynamic[typeSite]) { + this._formService + .formValues(this.obj, this.allTypesSiteConfig[typeSite]) + .subscribe((formValue) => { + this._formService.patchValuesInFormGroup(this.objFormsDynamic[typeSite], formValue); + this.isInitialzedObjFormDynamic[typeSite] = false; + }); + } // pour donner la valeur de l'objet au formulaire - this.obj.formValues().subscribe((formValue) => { - this.objForm.patchValue(formValue); - this.setDefaultFormValue(); - // reset geom ? - }); } keepNames() { @@ -242,10 +424,25 @@ export class MonitoringFormComponent implements OnInit { } /** TODO améliorer site etc.. */ - onSubmit() { - const action = this.obj.id - ? this.obj.patch(this.objForm.value) - : this.obj.post(this.objForm.value); + onSubmit(isAddChildrend = false) { + isAddChildrend + ? (this.bSaveAndAddChildrenSpinner = this.bAddChildren = true) + : (this.bSaveSpinner = true); + // if (this.obj.objectType == 'site') { + // this.dataComplement = { ...this.typesSiteConfig, types_site: this.idsTypesSite }; + // } + let objFormValueGroup = {}; + this.isSiteObject + ? (objFormValueGroup = this._formService.flattenFormGroup(this.objForm)) + : (objFormValueGroup = this.objForm.value); + // this.obj.objectType == 'site' + // ? Object.assign(this.obj.config['specific'], this.schemaUpdate) + // : null; + + // On merge l'objet avec les nouvelles valeurs issues du formulaire et les propriétés mises de cotés mais qui doivent être conservées + const finalObject = Utils.mergeObjects(this.remainingTypeSiteProp, objFormValueGroup); + this.isSiteObject ? (finalObject['types_site'] = Array.from(this.idsTypesSite)) : null; + const action = this.obj.id ? this.obj.patch(finalObject) : this.obj.post(finalObject); const actionLabel = this.obj.id ? 'Modification' : 'Création'; action.subscribe((objData) => { this._commonService.regularToaster('success', this.msgToaster(actionLabel)); @@ -273,6 +470,16 @@ export class MonitoringFormComponent implements OnInit { onCancelEdit() { if (this.obj.id) { + const urlTree = this._router.parseUrl(this._router.url); + const urlWithoutParams = urlTree.root.children['primary'].segments + .map((it) => it.path) + .join('/'); + this._router.navigate([urlWithoutParams]); + + // this._geojsonService.removeAllFeatureGroup(); + this.obj.geometry == null + ? this._geojsonService.setMapDataWithFeatureGroup([this._geojsonService.sitesFeatureGroup]) + : this._geojsonService.setMapBeforeEdit(this.obj.geometry); this.bEditChange.emit(false); } else { this.navigateToParent(); @@ -281,13 +488,11 @@ export class MonitoringFormComponent implements OnInit { onDelete() { this.bDeleteSpinner = true; - this._commonService.regularToaster('info', this.msgToaster('Suppression')); - this.obj.delete().subscribe((objData) => { this.bDeleteSpinner = this.bDeleteModal = false; this.obj.deleted = true; this.objChanged.emit(this.obj); - + this._commonService.regularToaster('info', this.msgToaster('Suppression')); setTimeout(() => { this.navigateToParent(); }, 100); @@ -295,6 +500,10 @@ export class MonitoringFormComponent implements OnInit { } onObjFormValueChange(event) { + // Check si types_site est modifié + if (event.types_site != null && event.types_site.length != this.idsTypesSite.size) { + this.updateTypeSiteForm(); + } const change = this.obj.change(); if (!change) { return; @@ -304,6 +513,21 @@ export class MonitoringFormComponent implements OnInit { }, 100); } + onObjFormValueChangeDynamic(event, typeSite) { + this.objForm = this._formService.addMultipleFormGroupsToObjForm( + this.objFormsDynamic, + this.objForm + ); + const change = this.obj.change(); + if (!change) { + return; + } + + setTimeout(() => { + change({ objForm: this.objFormsDynamic[typeSite], meta: this.meta }); + }, 100); + } + procesPatchUpdateForm() { this.objForm.patchValue({ patch_update: this.objForm.value.patch_update + 1 }); } @@ -317,4 +541,317 @@ export class MonitoringFormComponent implements OnInit { // patch pour recalculers this.procesPatchUpdateForm(); } + + updateTypeSiteForm() { + this.objForm.controls['types_site'].valueChanges + .pipe(distinctUntilChanged()) + .subscribe((idsTypeSite) => { + if (idsTypeSite && idsTypeSite.length == 0) { + // suppresson de tous les champs dynamiques si le champs est vide + this.removeAllDynamicGroups(); + } else { + // // Suppressin des formGroup des idSite déselectionnés + Object.keys(this.objFormsDynamic).forEach((key) => { + if (!idsTypeSite.includes(parseInt(key))) { + this.isInitialzedObjFormDynamic[key] = true; + delete this.objFormsDynamic[key]; + } + }); + this.idsTypesSite = new Set(idsTypeSite); + this.typesSiteConfig = {}; + // creation des nouveaux formGroup + idsTypeSite.forEach((idTypeSite) => { + this.typesSiteConfig[idTypeSite] = this.allTypesSiteConfig[idTypeSite]; + if (!this.objFormsDynamic[idTypeSite]) { + // Si dans la liste de type de site un nouveau type de site est ajouté alors on créé un formGroup + this.objFormsDynamic[idTypeSite] = this._formBuilder.group({}); + const objFormDefinition = this.initObjFormDefiniton( + this.typesSiteConfig[idTypeSite], + this.meta + ); + this.objFormsDefinitionDynamic[idTypeSite] = objFormDefinition; + this.isInitialzedObjFormDynamic[idTypeSite] = true; + } + }); + } + this.objForm = this._formService.addMultipleFormGroupsToObjForm( + this.objFormsDynamic, + this.objForm + ); + + const change = this.obj.change(); + if (!change) { + return; + } + + setTimeout(() => { + change({ objForm: this.objForm, meta: this.meta }); + }, 100); + }); + } + + initPermission() { + // Si les permissions n'ont pas été initialisées + if (this.currentUser.moduleCruved == undefined) { + this.currentUser.moduleCruved = this._configService.moduleCruved(this.obj.moduleCode); + } + + // Calcul du nombre d'enfants pour limiter l'action de suppression + const nb_childrens = + this.obj.properties['nb_sites'] || + 0 + this.obj.properties['nb_visits'] || + 0 + this.obj.properties['nb_observations'] || + 0; + if (this.obj.objectType == 'module') { + this.canDelete = false; // On ne peut pas supprimer un module + } else if (this.obj.cruved['D'] && nb_childrens > 0) { + this.canDelete = false; // On ne peut pas supprimer un objet s'il a des enfants + this.toolTipNotAllowed = TOOLTIPMESSAGEALERT_CHILD; + } else { + this.canDelete = this.obj.cruved['D']; + } + + this.canUpdate = + this.obj.objectType == 'module' + ? this.currentUser?.moduleCruved[this.obj.objectType]['U'] > 0 + : this.obj.cruved['U']; + } + + notAllowedMessage() { + this._commonService.translateToaster( + 'warning', + "Vous n'avez pas les permissions nécessaires pour éditer l'objet" + ); + } + + initObjFormDefiniton(schema: JsonData, meta: JsonData) { + const objectFormDefiniton = this._dynformService + .formDefinitionsdictToArray(schema, this.meta) + .filter((formDef) => formDef.type_widget) + .sort((a, b) => { + if (a.attribut_name === 'medias') return 1; + if (b.attribut_name === 'medias') return -1; + return 0; + }) + .sort((a, b) => { + if (a.attribut_name === 'types_site') return 1; + if (b.attribut_name === 'types_site') return -1; + return 0; + }); + return objectFormDefiniton; + } + + /** + * Initializes some variables for the component based on the given `MonitoringObject`. + * @param obj The `MonitoringObject` used to initialize the variables. + * @returns An object with the initialized variables. + */ + initializeVariables(obj: MonitoringObject) { + this.isSiteObject = obj.objectType === 'site'; + this.isEditObject = obj.id !== undefined && obj.id !== null; + this.hasDynamicGroups = this.isSiteObject && obj.properties['types_site'].length > 0; + + this.geomCalculated = this.obj.properties.hasOwnProperty('is_geom_from_child') + ? this.obj.properties['is_geom_from_child'] + : false; + + if (this.geomCalculated) { + this.obj.geometry = null; + } + // Si mode édition initialisation du layer de l'objet en cours du composant carto + if (this.bEdit) { + this._geojsonService.setCurrentmapData(this.obj.geometry, this.geomCalculated); + } + } + + /** + * Initializes type site config from generic config and specific config objects. + * @param genericConfig Generic config object + * @param specificConfig Specific config object + * @param typesSiteConfig Type site config object + * @param propertiesTypesSite Properties types site object + * @returns Observable of specific config object, generic config, type site config and set of ids of type site objects + */ + initializeTypeSiteConfig( + genericConfig: JsonData, + specificConfigInit: JsonData, + typesSiteConfigInit: JsonData, + propertiesTypesSite: any + ) { + const initTypeSiteConfigData = this.initTypeSiteConfig( + specificConfigInit, + propertiesTypesSite, + typesSiteConfigInit + ); + const { idsTypesSite, typesSiteConfig } = initTypeSiteConfigData; + const allTypesSiteConfig = typesSiteConfig; + const idsTypesSiteSet = new Set(idsTypesSite); + this.initializeSpecificConfig( + genericConfig, + specificConfigInit, + typesSiteConfig, + allTypesSiteConfig + ); + this.allTypesSiteConfig = allTypesSiteConfig; + this.idsTypesSite = idsTypesSiteSet; + // On met de coté l'ensemble des propriétés restantes et notamment (les champs "additional_data_keys" et "ids_types_site") + const mergeConfig = Utils.mergeObjects(specificConfigInit, genericConfig); + this.remainingTypeSiteProp = Utils.getRemainingKeys(this.obj.properties, mergeConfig); + } + + /** + * Initializes specific config from generic config and type site configs. + * @param genericConfig Generic config object + * @param specificConfig Specific config object + * @param configTypeSite Type site config object + * @param allTypesSiteConfig All type site config object + */ + + initializeSpecificConfig( + genericConfig: JsonData, + specificConfig: JsonData, + configTypeSite: JsonData = {}, + allTypesSiteConfig: JsonData = {} + ) { + const cleanSpecificConfig = this.initSpecificConfig( + specificConfig, + configTypeSite, + allTypesSiteConfig + ); + const confiGenericSpec = Utils.mergeObjects(cleanSpecificConfig, genericConfig); + this.specificConfig = specificConfig; + this.confiGenericSpec = confiGenericSpec; + } + + /** + * Initializes type site config from generic config and type site properties. + * @param configSpecific Generic config object + * @param typeSiteProperties Type site property names array + * @param configTypesSite Type site config object + * @returns Observable of type site config object + */ + initTypeSiteConfig( + configSpecific: JsonData, + typeSiteProperties: string[], + configTypesSite: { [typeSiteId: string]: { display_properties: string[]; name: string } } + ): { idsTypesSite: number[]; typesSiteConfig: { [typeSiteId: string]: JsonData } } { + const idsTypesSite = []; + const typesSiteConfig: { [typeSiteId: string]: JsonData } = {}; + for (const keyTypeSite in configTypesSite) { + typesSiteConfig[keyTypeSite] = {}; + let typeSiteName = configTypesSite[keyTypeSite].name; + for (const prop of configTypesSite[keyTypeSite].display_properties) { + typesSiteConfig[keyTypeSite][prop] = configSpecific[prop]; + } + typeSiteProperties.includes(typeSiteName) ? idsTypesSite.push(parseInt(keyTypeSite)) : null; + } + return { idsTypesSite: idsTypesSite, typesSiteConfig: typesSiteConfig }; + } + + /** + * Initializes specific config from generic config and type site config, if any. + * @param configSpecific Generic config object + * @param configTypesSite Optional type site config object + * @param allTypesSiteConfig Optional type site config object containing all type sites + * @returns Observable of strongly typed specific config object + */ + initSpecificConfig( + configSpecific: JsonData, + configTypesSite: JsonData = {}, + allTypesSiteConfig: Record = {} + ) { + let specificConfig: JsonData = {}; + if (Object.keys(configTypesSite).length) { + const allTypeSiteConfigCombined = Object.assign({}, ...Object.values(allTypesSiteConfig)); + specificConfig = Utils.getRemainingProperties(allTypeSiteConfigCombined, configSpecific); + } else { + specificConfig = configSpecific; + } + return specificConfig; + } + + sortObjFormDefinition(displayProperties: string[], objFormDef: JsonData) { + // Tri des propriétés en fonction des displays properties + + // let displayProperties = [...(this.obj.configParam('display_properties') || [])]; + // TODO: Vérifier mais normalement plus nécessaire d'utiliser cette évaluation de condition (objFormDef ne devrait pas être nul ici) + if (!objFormDef) return; + if (displayProperties && displayProperties.length) { + displayProperties.reverse(); + objFormDef.sort((a, b) => { + let indexA = displayProperties.findIndex((e) => e == a.attribut_name); + let indexB = displayProperties.findIndex((e) => e == b.attribut_name); + return indexB - indexA; + }); + } + return objFormDef; + } + + initObjFormValues(obj, config, idsTypesSite = []) { + return this._formService.formValues(obj, config).pipe( + concatMap((genericFormValues) => { + if (idsTypesSite.length != 0) { + genericFormValues['types_site'] = idsTypesSite; + } + return of(genericFormValues); + }) + ); + } + + initObjFormSpecificValues(obj, config) { + return this._formService.formValues(obj, config); + } + + removeDynamicFormGroup(groupName: string): void { + // Remove form group from objFormsDynamic + delete this.objFormsDynamic[groupName]; + delete this.objFormsDefinitionDynamic[groupName]; + + // Remove form group from the dynamicGroups FormArray + const dynamicGroupsArray = this.objForm.get('dynamicGroups') as FormArray; + const index = dynamicGroupsArray.controls.findIndex( + (group) => group === this.objFormsDynamic[groupName] + ); + if (index !== -1) { + dynamicGroupsArray.removeAt(index); + } + } + + removeAllDynamicGroups(): void { + // Clear objFormsDynamic and objFormsDefinitionDynamic + this.objFormsDynamic = {}; + this.objFormsDefinitionDynamic = {}; + + // Clear controls inside dynamicGroups FormArray + const dynamicGroupsArray = this.objForm.get('dynamicGroups') as FormArray; + while (dynamicGroupsArray.length) { + dynamicGroupsArray.removeAt(0); // Remove controls from the beginning + } + } + + createFormWithDynamicGroups(objFormGroup): FormGroup { + const dynamicGroups = this._formBuilder.array([]); + objFormGroup.addControl('dynamicGroups', dynamicGroups); + return objFormGroup; + } + + // TODO: VERIFIER si on garde cette "method" pour vérifier la validité des formGroup liés aux types de sites + // Pour l'instant on choisi de ne garder que l'objForm qui contient le formArray dynamicGroup + // qui lui même contient l'équivalent de l'ensemble des formGroup liés aux types de site + areDynamicFormsValid(): boolean { + // Iterate through each objFormDynamic and check if it's valid + for (const typeSite in this.objFormsDynamic) { + if (this.objFormsDynamic.hasOwnProperty(typeSite)) { + const objFormDynamic = this.objFormsDynamic[typeSite]; + if (!objFormDynamic.valid) { + return false; // If any objFormDynamic is invalid, return false + } + } + } + return true; // If all objFormsDynamic are valid, return true + } + + ngOnDestroy() { + this.objForm.patchValue({ geometry: null }); + } } diff --git a/frontend/app/components/monitoring-lists/monitoring-list.component.spec.ts b/frontend/app/components/monitoring-lists/monitoring-list.component.spec.ts index d9be7770d..d7a5337cd 100644 --- a/frontend/app/components/monitoring-lists/monitoring-list.component.spec.ts +++ b/frontend/app/components/monitoring-lists/monitoring-list.component.spec.ts @@ -1,6 +1,5 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - import { MonitoringListComponent } from './monitoring-lists.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; describe('MonitoringListComponent', () => { let component: MonitoringListComponent; diff --git a/frontend/app/components/monitoring-lists/monitoring-lists.component.css b/frontend/app/components/monitoring-lists/monitoring-lists.component.css index 47d8bad7e..535d451fb 100644 --- a/frontend/app/components/monitoring-lists/monitoring-lists.component.css +++ b/frontend/app/components/monitoring-lists/monitoring-lists.component.css @@ -1,11 +1,21 @@ .object-link:hover { - color:lightblue; + color: lightblue; } .btn-height { - height: 40px; + height: 40px; } .btn-float-right { - margin: 5px 0; + margin: 5px 0; +} + +button:disabled { + cursor: not-allowed; + pointer-events: all !important; +} + +.isDisableBtn { + cursor: not-allowed; + text-decoration: none; } diff --git a/frontend/app/components/monitoring-lists/monitoring-lists.component.html b/frontend/app/components/monitoring-lists/monitoring-lists.component.html index f317a7990..9aa1536c6 100644 --- a/frontend/app/components/monitoring-lists/monitoring-lists.component.html +++ b/frontend/app/components/monitoring-lists/monitoring-lists.component.html @@ -1,61 +1,68 @@ -
- - - - + + - -
-
-
- - + filter_alt + + +
+
+
+
-
- -
-
- + - - diff --git a/frontend/app/components/monitoring-lists/monitoring-lists.component.ts b/frontend/app/components/monitoring-lists/monitoring-lists.component.ts index 50bb1a8d2..194d4178c 100644 --- a/frontend/app/components/monitoring-lists/monitoring-lists.component.ts +++ b/frontend/app/components/monitoring-lists/monitoring-lists.component.ts @@ -5,6 +5,8 @@ import { ConfigService } from '../../services/config.service'; import { MonitoringObject } from '../../class/monitoring-object'; import { Utils } from '../../utils/utils'; +import { TOOLTIPMESSAGEALERT } from '../../constants/guard'; +import { ListService } from '../../services/list.service'; @Component({ selector: 'pnx-monitoring-lists', @@ -18,9 +20,17 @@ export class MonitoringListComponent implements OnInit { @Output() bEditChange = new EventEmitter(); @Input() currentUser; + @Output() filtersChange: EventEmitter = new EventEmitter(); - activetab: string; + @Input() forceReload; + @Output() forceReloadChange = new EventEmitter(); + @Input() selectedObject; + @Output() selectedObjectChange: EventEmitter = new EventEmitter(); + + @Output() onDeleteRow: EventEmitter = new EventEmitter(); + + nbVisibleRows: Record = {}; frontendModuleMonitoringUrl; backendUrl: string; @@ -33,16 +43,18 @@ export class MonitoringListComponent implements OnInit { queyParamsNewObject = {}; // medias; - - @Input() objectsStatus: Object; - @Output() objectsStatusChange: EventEmitter = new EventEmitter(); - - constructor(private _configService: ConfigService) {} + canCreateChild: { [key: string]: boolean } = {}; + toolTipNotAllowed: string = TOOLTIPMESSAGEALERT; + constructor( + private _configService: ConfigService, + private _listService: ListService + ) {} ngOnInit() { - this._configService.init(this.obj.moduleCode).subscribe(() => { - this.initDataTable(); - }); + // Permet d'éviter une double initialisation du composant + // this._configService.init(this.obj.moduleCode).subscribe(() => { + // this.initDataTable(); + // }); } initDataTable() { @@ -58,52 +70,59 @@ export class MonitoringListComponent implements OnInit { this.backendUrl = this._configService.backendUrl(); this.children0Array = this.obj.children0Array(); - this.activetab = this.children0Array[0] && this.children0Array[0].objectType; + // datatable this.childrenDataTable = this.obj.childrenColumnsAndRows('display_list'); + // Initialisation nombre d'élément affiché dans la liste + Object.keys(this.childrenDataTable).forEach((chidrenType) => { + this.nbVisibleRows[chidrenType] = this.childrenDataTable[chidrenType].rows.length; + }); + this.initPermission(); // this.medias = this.obj.children['media'] && this.obj.children['media'].map(e => e.properties); } + initPermission() { + for (const child of this.children0Array) { + const childType = child['objectType']; + this.canCreateChild[childType] = this.currentUser?.moduleCruved[childType].C > 0; + } + } + onSelectedChildren(typeObject, event) { - this.objectsStatus[typeObject] = event; - let status_type = Utils.copy(this.objectsStatus); - status_type['type'] = typeObject; - this.objectsStatusChange.emit(status_type); + this.selectedObject = event; + this.selectedObjectChange.emit(event); + } + + onFilterChange(type, event) { + const nb_row = event['nb_row']; + this.nbVisibleRows[type] = nb_row; + } + + onDeleteRowChange(event) { + this.onDeleteRow.emit(event); } changeActiveTab(typeObject, tab) { - this.activetab = typeObject; + const activetab = this.children0Array[typeObject['index']]; // Réinitialisation des données selectés - this.objectsStatusChange.emit(this.reInitStatut()); + this._listService.listType = activetab['objectType']; + this._listService.tableFilters = + this._listService.arrayTableFilters$.getValue()[activetab['objectType']]; } - reInitStatut() { - let status_type = Utils.copy(this.objectsStatus); - for (let typeObject in status_type) { - if (Array.isArray(status_type[typeObject])) { - for (let i in status_type[typeObject]) { - try { - status_type[typeObject][i]['selected'] = false; - } catch (error) { - console.error(error.message, status_type[typeObject][i]); - } - } - } - } - return status_type; - } onbEditChanged(event) { this.bEditChange.emit(event); } displayNumber(chidrenType) { - if (!this.objectsStatus[chidrenType]) { + if (!this.childrenDataTable[chidrenType]) { return ''; } - const visibles = this.objectsStatus[chidrenType].filter((s) => s.visible); - const nbSelected = visibles.length; - const nb = this.obj.children[chidrenType].length; + + const nbSelected = this.nbVisibleRows[chidrenType]; + const nb = this.childrenDataTable[chidrenType]['rows'].length; + return nb == nbSelected ? `(${nb})` : `(${nbSelected}/${nb})`; } @@ -116,6 +135,13 @@ export class MonitoringListComponent implements OnInit { case 'obj': this.initDataTable(); break; + case 'forceReload': + if (cur == true) { + this.initDataTable(); + this.forceReload = false; + this.forceReloadChange.emit(false); + } + break; } } } diff --git a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.css b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.css new file mode 100644 index 000000000..f8f5803fb --- /dev/null +++ b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.css @@ -0,0 +1,81 @@ +.flex-container { + background-color: rgb(240, 240, 240); + display: flex; +} + +.flex-container > div { + width: 50%; + padding: 10px; + margin: 10px; +} + +.flex-container > div:first-child { + margin-right: 0; +} + +.scroll { + overflow-y: scroll; +} + +:host ::ng-deep .cadre { + background-color: white; + /* border: 1px solid grey;*/ + border-radius: 5px; + padding: 5px; + margin: 5px; + /* display: inline-block; */ +} + +/* TABLE */ + +.cell-link { + cursor: pointer; +} + +:host::ng-deep .datatable-body-row.active .datatable-row-group { + background-color: rgb(117, 227, 118) !important; +} + +.link:hover { + background-color: rgba(0, 0, 0, 0.2) !important; + transition: background-color 0.5; +} + +.link { + display: inline; + transition: background-color 0.5s; + border-radius: 5px; +} + +.header-filter-span > input { + width: 100%; +} + +.header-sort-span { + /* width: 100%; */ + cursor: pointer; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.header-sort-span:hover { + background-color: rgb(245, 245, 245); +} + +.icon-sort { + font-size: 1.2em; + float: right; +} + +:host::ng-deep .sort-btn { + display: none !important; +} + +.custom-dt { + box-shadow: none !important; +} + +:host ::ng-deep .hide-draw-form .leaflet-top.leaflet-left { + display: none; +} diff --git a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html new file mode 100644 index 000000000..28936bf4c --- /dev/null +++ b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html @@ -0,0 +1,22 @@ +
+
+ + + +
+
+ + + +
+
diff --git a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts new file mode 100644 index 000000000..cdc8773c2 --- /dev/null +++ b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts @@ -0,0 +1,50 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { tap, mergeMap, map, distinctUntilChanged } from 'rxjs/operators'; +import { SiteSiteGroup } from '../../interfaces/objObs'; +import { FormService } from '../../services/form.service'; +import { ApiGeomService } from '../../services/api-geom.service'; +import { ConfigJsonService } from '../../services/config-json.service'; +import { ObjectService } from '../../services/object.service'; +import { CommonService } from '@geonature_common/service/common.service'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'monitoring-map-list.component', + templateUrl: './monitoring-map-list.component.html', + styleUrls: ['./monitoring-map-list.component.css'], +}) +export class MonitoringMapListComponent { + // TODO: object needed to manage map + obj: any; + bEdit: boolean; + objForm: FormGroup; + heightMap: string = '80vh'; + // + displayMap: boolean = true; + siteSiteGroup: SiteSiteGroup | null = null; + apiService: ApiGeomService; + + constructor( + private _formService: FormService, + private _commonService: CommonService + ) {} + + ngAfterViewInit() { + const container = document.getElementById('object'); + const height = this._commonService.calcCardContentHeight(); + container.style.height = height - 40 + 'px'; + setTimeout(() => { + this.heightMap = height - 80 + 'px'; + }); + } + + onActivate(component) { + this._formService.currentFormMap + .pipe(distinctUntilChanged((prev, curr) => prev.obj === curr.obj)) + .subscribe((formMapObj) => { + this.obj = formMapObj.obj; + this.bEdit = formMapObj.bEdit; + this.objForm = formMapObj.frmGp; + }); + } +} diff --git a/frontend/app/components/monitoring-map/monitoring-map.component.css b/frontend/app/components/monitoring-map/monitoring-map.component.css index 36556bbab..0aa58a60a 100644 --- a/frontend/app/components/monitoring-map/monitoring-map.component.css +++ b/frontend/app/components/monitoring-map/monitoring-map.component.css @@ -1,3 +1,3 @@ :host ::ng-deep .hide-draw-form .leaflet-top.leaflet-left { - display: none; - } + display: none; +} diff --git a/frontend/app/components/monitoring-map/monitoring-map.component.html b/frontend/app/components/monitoring-map/monitoring-map.component.html index 98b74ab15..91849ee05 100644 --- a/frontend/app/components/monitoring-map/monitoring-map.component.html +++ b/frontend/app/components/monitoring-map/monitoring-map.component.html @@ -1,22 +1,22 @@
- - + - + > -->
diff --git a/frontend/app/components/monitoring-map/monitoring-map.component.spec.ts b/frontend/app/components/monitoring-map/monitoring-map.component.spec.ts index 27f646211..9d15dd824 100644 --- a/frontend/app/components/monitoring-map/monitoring-map.component.spec.ts +++ b/frontend/app/components/monitoring-map/monitoring-map.component.spec.ts @@ -1,6 +1,5 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - import { MonitoringMapComponent } from './monitoring-map.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; describe('MonitoringMapComponent', () => { let component: MonitoringMapComponent; diff --git a/frontend/app/components/monitoring-map/monitoring-map.component.ts b/frontend/app/components/monitoring-map/monitoring-map.component.ts index ff189651b..5fbdb760b 100644 --- a/frontend/app/components/monitoring-map/monitoring-map.component.ts +++ b/frontend/app/components/monitoring-map/monitoring-map.component.ts @@ -1,15 +1,23 @@ import { Component, OnInit, Input, Output, EventEmitter, SimpleChanges } from '@angular/core'; +import { merge } from 'rxjs'; +import { filter, distinctUntilChanged } from 'rxjs/operators'; +import { isEqual } from 'lodash'; + import { FormGroup } from '@angular/forms'; import { MonitoringObject } from '../../class/monitoring-object'; import { Layer, svg, Path } from 'leaflet'; import { ConfigService } from '../../services/config.service'; import { DataMonitoringObjectService } from '../../services/data-monitoring-object.service'; +import { GeoJSONService } from '../../services/geojson.service'; +import { Popup } from '../../utils/popup'; +import { ActivatedRoute } from '@angular/router'; import { MapService } from '@geonature_common/map/map.service'; import { MapListService } from '@geonature_common/map-list/map-list.service'; import { Utils } from '../../utils/utils'; import * as L from 'leaflet'; +import { ListService } from '../../services/list.service'; @Component({ selector: 'pnx-monitoring-map', @@ -20,14 +28,9 @@ export class MonitoringMapComponent implements OnInit { @Input() bEdit: boolean; @Input() obj: MonitoringObject; - @Input() objectsStatus: Object; - @Output() objectsStatusChange: EventEmitter = new EventEmitter(); - + @Input() selectedObject: Object; @Input() objForm: FormGroup; - @Input() sites: {}; - @Input() sitesGroup: {}; - @Input() heightMap; bListen = true; @@ -38,12 +41,9 @@ export class MonitoringMapComponent implements OnInit { currentSiteId: Number; publicDisplaySitesGroup: boolean = false; - // Layer des contours groupes de sites - sitesGroupEmprisePoly = []; - sitesGroupEmpriseLabel = []; + listObjectSubscription: any; // todo mettre en config - styles = { hidden: { opacity: 0, @@ -81,88 +81,124 @@ export class MonitoringMapComponent implements OnInit { protected _mapService: MapService, private _configService: ConfigService, private _data: DataMonitoringObjectService, - private _mapListService: MapListService + private _mapListService: MapListService, + private _geojsonService: GeoJSONService, + private _popup: Popup, + public listService: ListService, + private _route: ActivatedRoute ) {} - ngOnInit() {} - - initSites() { - this.removeLabels(); - const layers = this._mapService.map['_layers']; - for (const key of Object.keys(layers)) { - const layer = layers[key]; - try { - layer.unbindTooltip(); - } catch {} - } - setTimeout(() => { - this.initPanes(); - if (this.sites && this.sites['features']) { - this.initSitesStatus(); - for (const site of this.sites['features']) { - this.setPopup(site.id); - const layer = this.findSiteLayer(site.id); - // pane - const fClick = this.onLayerClick(site); - layer.off('click', fClick); - layer.on('click', fClick); - // - layer.removeFrom(this._mapService.map); - layer.addTo(this._mapService.map); + ngOnInit() { + // gestion des interractions carte list + const tableType = ['sites_group', 'site']; + + // Souscription aux sujets : + // * type de l'onglet actif du datatable + // * filtre des datatables + // * préfiltres + this.listObjectSubscription = merge( + this.listService.listType$.pipe(filter(() => this.listService.listType$.getValue() != null)), + this.listService.tableFilters$.pipe( + // Si le datatable n'est ni un site, ni un groupe de sites + // on ne tient pas compte des filtres pour la route geométrie + filter(() => tableType.indexOf(this.listService.listType) >= 0), + distinctUntilChanged(isEqual) + ) + ).subscribe(() => { + const listType = this.listService.listType$.getValue(); + const preFilters = this.listService.preFilters; + const tableFilters = this.listService.tableFilters$.getValue(); + // On attent l'initialisation des préfiltres et le type de datatable + if (listType !== null && preFilters !== null) { + // On attent l'initalisation des préfiltres pour les listes de types site et groupes de sites + if ((tableFilters === null && tableType.indexOf(listType) < 0) || tableFilters !== null) { + this.refresh_geom_data(); } - - this.setSitesStyle(); } - }, 0); + }); } - removeLabels() { - if (!this._mapService.map) { - return; - } - const layers = this._mapService.map['_layers']; - for (const key of Object.keys(layers)) { - const layer = layers[key]; - if (layer.options.permanent) { - layer.removeFrom(this._mapService.map); - } + refresh_geom_data() { + this._geojsonService.removeAllLayers(); + let displayObject; + // Choix des objets a afficher + if (this.bEdit && !this.obj.id) { + // Si création d'un nouvel objet on n'affiche rien + displayObject = undefined; + } else if (this.bEdit && this.obj.id) { + // Si modification affichage de l'objet en cours + displayObject = this.obj.objectType; + } else if (this.obj.objectType == 'module') { + // Si module affichage du type d'objet courant + displayObject = this.listService.listType; + } else if (this.obj.objectType == 'sites_group') { + // Si page détail d'un groupe de site affichage du groupe de site et de ces enfants + displayObject = 'sites_group_with_child'; + } else { + // Sinon affichage des sites + displayObject = 'site'; + } + + this._geojsonService.removeAllFeatureGroup(); + if (displayObject == 'site') { + const params = { + ...this.listService.getPrefilterByType(displayObject), + ...this.listService.tableFilters$.getValue(), + }; + this._geojsonService.getSitesGroupsChildGeometries( + this.onEachFeatureSite(this.buildQueryParams('site')), + params + ); + } else if (displayObject == 'sites_group') { + const params = { + ...this.listService.getPrefilterByType(displayObject), + ...this.listService.tableFilters$.getValue(), + }; + this._geojsonService.getSitesGroupsGeometries( + this.onEachFeatureGroupSite(this.buildQueryParams('sites_group')), + params + ); + } else if (displayObject == 'sites_group_with_child') { + const paramsSitesGroup = { + ...this.listService.getPrefilterByType('sites_group'), + ...this.listService.tableFilters$.getValue(), + }; + const paramsSite = { + ...this.listService.getPrefilterByType('site'), + ...this.listService.tableFilters$.getValue(), + }; + this._geojsonService.getSitesGroupsGeometriesWithSites( + this.onEachFeatureGroupSite(this.buildQueryParams('sites_group')), + this.onEachFeatureSite(this.buildQueryParams('site')), + paramsSitesGroup, + paramsSite + ); } } - onEachFeature = (feature, layer) => { - const mapLabelFieldName = this.obj.configParam('map_label_field_name'); - if (!mapLabelFieldName) { - return; - } - const textValue = feature.resolvedProperties[mapLabelFieldName]; - if (!textValue) { - return; - } + buildQueryParams(displayObject: string) { + // Construction des queryParams + // Important pour le paramètre parents_path qui est essentiel pour le breadcrumb + let parents_path = ['module']; + let current_object = this._route.snapshot.params['objectType']; - let coordinates; - if (feature.geometry.type == 'Point') { - coordinates = layer.getLatLng(); - } else { - coordinates = layer.getBounds().getCenter(); + if (!parents_path.includes(current_object) && current_object !== displayObject) { + parents_path.push(current_object); } + return { parents_path: parents_path }; + } - var text = L.tooltip({ - permanent: true, - direction: 'top', - className: 'text', - }) - .setContent(textValue) - .setLatLng(coordinates); - layer.bindTooltip(text).openTooltip(); - text.addTo(this._mapService.map); - }; + onEachFeatureSite(queryParams) { + return (feature, layer) => { + const popup = this._popup.setSitePopup(this.obj.moduleCode, feature, queryParams); + layer.bindPopup(popup); + }; + } - onLayerClick(site) { - return (event) => { - const id = this.selectedSiteId === site.id ? -1 : site.id; - this.setSelectedSite(id); - this.bListen = false; - this.objectsStatusChange.emit(Utils.copy(this.objectsStatus)); + onEachFeatureGroupSite(queryParams) { + return (feature, layer) => { + const popup = this._popup.setSiteGroupPopup(this.obj.moduleCode, feature, queryParams); + layer.bindPopup(popup); }; } @@ -179,202 +215,37 @@ export class MonitoringMapComponent implements OnInit { } } - initSitesStatus() { - if (!this.objectsStatus['site']) { - this.objectsStatus['site'] = []; - } - const $this = this; - this.sites['features'].forEach((site) => { - const status = $this.objectsStatus['site'].find((s) => s.id === site.id); - if (status) { - return; - } - - $this.objectsStatus['site'].push({ - selected: false, - visible: true, - id: site.id, - }); - }); - } - - setSelectedSite(id) { - if (id == this.selectedSiteId) { - return; - } - - // Get old select site - let old_s_site = this.objectsStatus['site'].filter((site) => site.id == this.selectedSiteId); - if (old_s_site.length > 0) { - old_s_site[0]['selected'] = false; - this.setSiteStyle(old_s_site[0]); - } - - // Get new select site - let new_s_site = this.objectsStatus['site'].filter((site) => site.id == id); - if (new_s_site.length > 0) { - new_s_site[0]['selected'] = true; - this.setSiteStyle(new_s_site[0]); - } - this.selectedSiteId = id; - } - - setSitesStyle() { - const objectType = this.objectsStatus['type']; - let openPopup = true; - - if (this._mapService.map) { - this.objectsStatus[objectType] && - this.objectsStatus[objectType].forEach((status) => { - this.setSiteStyle(status, openPopup, objectType); - }); - } - // Si le dessin des groupes de sites est actif calcul de l'aire + setSitesStyle(objectType = 'site') { if (this._configService.config()[this.obj.moduleCode]['module']['b_draw_sites_group']) { this.publicDisplaySitesGroup = true; } } - setSiteStyle(status, openPopup = true, objectType = 'site') { - /* - Défini le style des éléments - statuts = statut de l'élément provient de objectsStatus - openPopup = indique si la popup doit s'afficher - */ - const map = this._mapService.map; - let layer = this.findSiteLayer(status.id, objectType); - - if (!layer) { + ngOnChanges(changes: SimpleChanges) { + if (!this._mapService.map) { return; } - const style_name = !status['visible'] - ? 'hidden' - : status['current'] - ? 'current' - : status['selected'] - ? 'selected' - : this.bEdit - ? 'edit' - : 'default'; - - const style = this.styles[style_name] || this.styles['default']; - - style['pane'] = this.panes[style_name]; - style['renderer'] = this.renderers[style_name]; - // layer.removeFrom(map); - layer.setStyle(style); - // layer.addTo(map); - - if (status['selected']) { - this._mapListService.zoomOnSelectedLayer(map, layer); - } - - if (status['selected'] && openPopup == true) { - if (!(layer as any)._popup) { - this.setPopup(status.id); - layer = this.findSiteLayer(status.id, objectType); + if (Object.keys(changes).includes('selectedObject')) { + if (!this.selectedObject) { + return; } - layer.openPopup(); - } - - if (!status['visible'] || !status['selected']) { - layer.closePopup(); - } - - // Affichage des tooltips uniquement si la feature est visible - if (layer.getTooltip) { - var toolTip = layer.getTooltip(); - if (style_name == 'hidden') { - if (toolTip) { - map.closeTooltip(toolTip); - } - } else { - if (toolTip) { - map.addLayer(toolTip); + if (this.obj.objectType == 'module' && Object.keys(this.selectedObject).length > 0) { + if (this.listService.listType == 'sites_group') { + this._geojsonService.selectSitesGroupLayer(this.selectedObject['id'], true); + } else if (this.listService.listType == 'site') { + this._geojsonService.selectSitesLayer(this.selectedObject['id'], true); } } } - } - - findSiteLayer(id, objectType = 'site'): Path { - const layers = this._mapService.map['_layers']; - const layerKey = Object.keys(layers) - .filter((key) => { - const monitoringObject = layers[key] && layers[key].feature; - return monitoringObject && monitoringObject.objectType == objectType; - }) - .find((key) => { - const feature = layers[key] && layers[key].feature; - return feature && (feature['id'] === id || feature.properties['id'] === id); - }); - return layerKey && layers[layerKey]; - } - - findSiteLayers(value, property): Array { - const layers = this._mapService.map['_layers']; - - let filterlayers = Object.keys(layers) - .filter( - (key) => layers[key].feature && layers[key]['feature']['properties'][property] == value - ) - .map((key) => ({ [key]: layers[key] })); - - if (filterlayers.length > 0) { - return Object.assign(...(filterlayers as [Object])); - } - return []; - } - - setPopup(id) { - const layer = this.findSiteLayer(id); - if (layer['_popup']) { - return; + if (Object.keys(changes).includes('bEdit')) { + this.setSitesStyle(this.obj.objectType); } - // TODO verifier si le fait de spécifier # en dur - // Ne pose pas de soucis pour certaine configuration - const url = [ - '#', - this._configService.frontendModuleMonitoringUrl(), - 'object', - this.obj.moduleCode, - 'site', - layer['feature'].properties.id_base_site, - ].join('/'); - - const sPopup = ` -
-

${layer['feature'].properties.base_site_name}

- ${layer['feature'].properties.description || ''} -
- `; - - layer.bindPopup(sPopup).closePopup(); } - ngOnChanges(changes: SimpleChanges) { - if (!this._mapService.map) { - return; - } - for (const propName of Object.keys(changes)) { - const chng = changes[propName]; - const cur = chng.currentValue; - const pre = chng.currentValue; - switch (propName) { - case 'objectsStatus': - if (!this.bListen) { - this.bListen = true; - } else { - this.setSitesStyle(); - } - break; - case 'bEdit': - this.setSitesStyle(); - - break; - case 'sites': - this.initSites(); - } - } + ngOnDestroy() { + // Réinitalisations des paramètres + this.listService.reinitializeObservables(); + this.listObjectSubscription.unsubscribe(); } } diff --git a/frontend/app/components/monitoring-object/monitoring-object.component.css b/frontend/app/components/monitoring-object/monitoring-object.component.css index a48080d41..effbbfb96 100644 --- a/frontend/app/components/monitoring-object/monitoring-object.component.css +++ b/frontend/app/components/monitoring-object/monitoring-object.component.css @@ -35,7 +35,6 @@ font-weight: bold; } - :host ::ng-deep .cadre { background-color: white; /* border: 1px solid grey;*/ diff --git a/frontend/app/components/monitoring-object/monitoring-object.component.html b/frontend/app/components/monitoring-object/monitoring-object.component.html index b34e54fff..366a34c68 100644 --- a/frontend/app/components/monitoring-object/monitoring-object.component.html +++ b/frontend/app/components/monitoring-object/monitoring-object.component.html @@ -2,37 +2,23 @@

Chargement en cours

- +
- +
- +
- +

Le module n'est pas encore configuré.

@@ -40,30 +26,16 @@

Chargement en cours

Veuillez éditer le module et renseigner les champs requis.

- - + + - +
- + \ No newline at end of file diff --git a/frontend/app/components/monitoring-object/monitoring-object.component.ts b/frontend/app/components/monitoring-object/monitoring-object.component.ts index eab3efe1a..cf7c66b6b 100644 --- a/frontend/app/components/monitoring-object/monitoring-object.component.ts +++ b/frontend/app/components/monitoring-object/monitoring-object.component.ts @@ -1,5 +1,15 @@ import { Observable, of, forkJoin } from 'rxjs'; -import { mergeMap, concatMap } from 'rxjs/operators'; +import { + mergeMap, + concatMap, + map, + tap, + take, + takeUntil, + distinctUntilChanged, + catchError, + skipWhile, +} from 'rxjs/operators'; import { MonitoringObject } from '../../class/monitoring-object'; import { Component, OnInit } from '@angular/core'; @@ -13,8 +23,13 @@ import { DataUtilsService } from '../../services/data-utils.service'; import { AuthService, User } from '@geonature/components/auth/auth.service'; import { CommonService } from '@geonature_common/service/common.service'; import { MapService } from '@geonature_common/map/map.service'; +import { ObjectService } from '../../services/object.service'; import { Utils } from '../../utils/utils'; +import { ConfigJsonService } from '../../services/config-json.service'; +import { GeoJSONService } from '../../services/geojson.service'; +import { ListService } from '../../services/list.service'; + @Component({ selector: 'pnx-object', templateUrl: './monitoring-object.component.html', @@ -23,23 +38,25 @@ import { Utils } from '../../utils/utils'; export class MonitoringObjectComponent implements OnInit { obj: MonitoringObject; module: MonitoringObject; - sites; - sitesGroup; + + selectedObject: Object = undefined; + forceReload: boolean = false; backendUrl: string; frontendModuleMonitoringUrl: string; objForm: FormGroup; + checkEditParam: boolean; bEdit = false; bLoadingModal = false; currentUser: User; - objectsStatus: Object = {}; heightMap; moduleSet = false; + bDeleteModal = false; constructor( private _route: ActivatedRoute, @@ -49,7 +66,10 @@ export class MonitoringObjectComponent implements OnInit { private _formBuilder: FormBuilder, public mapservice: MapService, private _auth: AuthService, - private _commonService: CommonService + private _commonService: CommonService, + private _evtObjService: ObjectService, + private _geojsonService: GeoJSONService, + public _listService: ListService ) {} ngAfterViewInit() { @@ -85,11 +105,19 @@ export class MonitoringObjectComponent implements OnInit { }), mergeMap(() => { return this.getParents(); // récupération des données de l'object selon le type (module, site, etc..) + }), + tap(() => { + // if (this.obj.objectType == 'sites_group') { + // this._geojsonService.removeAllFeatureGroup(); + // this.obj.geometry + // ? this._geojsonService.setGeomSiteGroupFromExistingObject(this.obj.geometry) + // : null; + // } }) ) .subscribe(() => { this.obj.initTemplate(); // pour le html - + this.bEdit = this.checkEditParam == true ? true : false; // si on est sur une création (pas d'id et id_parent ou pas de module_code pour module (root)) this.bEdit = this.bEdit || @@ -97,122 +125,86 @@ export class MonitoringObjectComponent implements OnInit { (!this.obj.id && !!this.obj.parentId); this.bLoadingModal = false; // fermeture du modal this.obj.bIsInitialized = true; // obj initialisé - - if (!this.sites || this.obj.children['site']) { - this.initSites(); - } else { - this.initObjectsStatus(); - } }); } + onEachFeatureSite() { + return (feature, layer) => {}; + } + initCurrentUser() { this.currentUser = this._auth.getCurrentUser(); this.currentUser['moduleCruved'] = this._configService.moduleCruved(this.obj.moduleCode); } - getModuleSet() { - // Verifie si le module est configué - this.module.get(0).subscribe(() => { - const schema = this._configService.schema(this.module.moduleCode, 'module'); - const moduleFieldList = Object.keys( - this._configService.schema(this.module.moduleCode, 'module') - ).filter((key) => schema[key].required); - this.moduleSet = moduleFieldList.every( - (v) => ![null, undefined].includes(this.module.properties[v] || this.obj.properties[v]) - ); - }); + getModuleSet(): Observable { + // récupération des données de l'object selon le type (module, site, etc..) + return this.module.get(0).pipe( + mergeMap(() => + this.getDataObject().pipe( + tap(() => { + const schema = this._configService.schema(this.module.moduleCode, 'module'); + const moduleFieldList = Object.keys( + this._configService.schema(this.module.moduleCode, 'module') + ).filter((key) => schema[key].required); + this.moduleSet = moduleFieldList.every( + (v) => + ![null, undefined].includes(this.module.properties[v] || this.obj.properties[v]) + ); + + this.initPreFilters(); + }) + ) + ) + ); } - initSites() { - return this.module.get(1).subscribe(() => { - // TODO liste indépendantes carte et listes - - // affichage des groupes de site uniquement si l'objet est un module - if (this.obj.objectType == 'module' && this.obj['children']['sites_group']) { - const sitesGroup = this.obj['children']['sites_group']; - this.sitesGroup = { - features: sitesGroup.map((group) => { - group['id'] = group['properties']['id_sites_group']; - group['type'] = 'Feature'; - return group; - }), - type: 'FeatureCollection', - }; - } - // affichage des sites du premier parent qui a des sites dans l'odre de parent Path - let sites = null; - let cur = this.obj; - do { - sites = cur['children']['site']; - cur = cur.parent(); - } while (!!cur && !sites); - - if (!sites) { - return; - } - this.sites = { - features: sites.map((site) => { - site['id'] = site['properties']['id_base_site']; - site['type'] = 'Feature'; - return site; - }), - type: 'FeatureCollection', - }; - this.initObjectsStatus(); - }); - } + initPreFilters() { + // modules + const queryParams = this._route.snapshot.queryParams || {}; - initObjectsStatus() { - const objectsStatus = {}; - for (const childrenType of Object.keys(this.obj.children)) { - objectsStatus[childrenType] = this.obj.children[childrenType].map((child) => { - return { - id: child.id, - selected: false, - visible: true, - current: false, - }; - }); + let pre_filters = this._listService.preFilters; + pre_filters['site']['types_site'] = Object.keys( + this._configService.config()[this.obj.moduleCode]['module']['types_site'] + ); + if (this.obj.objectType == 'module') { + pre_filters['sites_group']['modules'] = this.obj.properties['id_module']; + } + if (this.obj.objectType == 'sites_group') { + pre_filters['sites_group']['id_sites_group'] = this.obj.id; + pre_filters['site']['id_sites_group'] = this.obj.id; + } else if (this.obj.objectType == 'site') { + pre_filters['site']['id_base_site'] = this.obj.id; + } else if (this.obj['siteId'] !== undefined) { + // affichage du site parent + pre_filters['id_base_site'] = this.obj['siteId']; + } else if (queryParams['id_base_site'] !== undefined) { + // récupération du site parent via l'url + pre_filters['site']['id_base_site'] = queryParams['id_base_site']; + } else if (queryParams['siteId'] !== undefined) { + // récupération du site parent via l'url + pre_filters['site']['id_base_site'] = queryParams['siteId']; } - // init site status - if (this.obj.siteId) { - objectsStatus['site'] = []; - this.sites['features'].forEach((f) => { - // determination du site courrant - let cur = false; - if (f.properties.id_base_site == this.obj.siteId) { - cur = true; - } + this._listService.preFilters = pre_filters; - objectsStatus['site'].push({ - id: f.properties.id_base_site, - selected: false, - visible: true, - current: cur, - }); - }); + // L'objet n'a aucun enfant initialisation du paramètre listType + if (Object.keys(this.obj.children || []).length == 0) { + this._listService.tableFilters = {}; + if (this.obj.objectType == 'sites_group') { + this._listService.listType = 'sites_group'; + } else { + this._listService.listType = 'site'; + } + } else { + this._listService.listType = this.obj.children0Array()[0].objectType; } - - this.objectsStatus = objectsStatus; } - // initRoutesQueryParams() { - - // return this._route.queryParamMap.pipe( - // mergeMap((params) => { - // this.obj.parentsPath = params.getAll("parents_path") || []; - // return of(true); - // }) - // ); - // } - initRoutesParams() { return this._route.paramMap.pipe( mergeMap((params) => { const objectType = params.get('objectType') ? params.get('objectType') : 'module'; - this.obj = new MonitoringObject( params.get('moduleCode'), objectType, @@ -229,6 +221,11 @@ export class MonitoringObjectComponent implements OnInit { ); this.objForm = this._formBuilder.group({}); + if (params.get('edit')) { + this.checkEditParam = Boolean(params.get('edit')); + } else { + this.checkEditParam = false; + } // query param snapshot // this.obj.parentId = params.get('parentId') && parseInt(params.get('parentId')); @@ -239,6 +236,22 @@ export class MonitoringObjectComponent implements OnInit { initConfig(): Observable { return this._configService.init(this.obj.moduleCode).pipe( + concatMap(() => { + if (this.obj.objectType == 'site' && this.obj.id != null) { + return this._objService + .configService() + .loadConfigSpecificConfig(this.obj) + .pipe( + tap((config) => { + this.obj.template_specific = this._objService + .configService() + .addSpecificConfig(config); + }) + ); + } else { + return of(null); + } + }), mergeMap(() => { this.frontendModuleMonitoringUrl = this._configService.frontendModuleMonitoringUrl(); this.backendUrl = this._configService.backendUrl(); @@ -248,8 +261,17 @@ export class MonitoringObjectComponent implements OnInit { } initData(): Observable { - this.getModuleSet(); - return this._dataUtilsService.getInitData(this.obj.moduleCode); + // Réinitialisation des services + this._listService.reinitializeObservables(); + + return of(true).pipe( + mergeMap(() => { + return this.getModuleSet(); + }), + mergeMap(() => { + return this._dataUtilsService.getInitData(this.obj.moduleCode); + }) + ); } getDataObject(): Observable { @@ -273,9 +295,13 @@ export class MonitoringObjectComponent implements OnInit { onObjChanged(obj: MonitoringObject) { this.obj = obj; - if (obj['objectType'] === 'site') { - this.initSites(); - } - this.getModuleSet(); + this.getModuleSet().subscribe(); + } + + onDeleteRowChange(event) { + this.getDataObject().subscribe((obj) => { + this.forceReload = true; + this.onObjChanged(obj); + }); } } diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.css b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.css new file mode 100644 index 000000000..14aa0b367 --- /dev/null +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.css @@ -0,0 +1,37 @@ +table { + font-size: small; +} + +th { + text-align: right; +} + +.key { + font-weight: bold; +} + +td { + padding-left: 20px; +} + +.small-icon { + font-size: 18px; +} + +.medias-tab { + margin: 10px; +} + +.hide-spinner { + display: none; +} + +::ng-deep .cdk-global-overlay-wrapper, +::ng-deep .cdk-overlay-container { + z-index: 99999 !important; +} + +button:disabled { + cursor: not-allowed; + pointer-events: all !important; +} diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html new file mode 100644 index 000000000..8c0a039de --- /dev/null +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html @@ -0,0 +1,103 @@ +
+
+ + +
+ + + + + +
+ {{ fields[fieldName] }} + help + {{ selectedObj[fieldName] }}
+
+
+ +
+ + + + + +
+ {{ specificFields[fieldName.value] }} + help + {{ selectedObj.data[fieldName.value] }}
+
+
+ + + +
+
+ {{ media.title_fr }} + + ({{ ms.typeMedia(media) }} par {{ media.author }}) + +
+

{{ media.description_fr }}

+
+ + +
+
+
+
+
+ + +
+
diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.spec.ts b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.spec.ts new file mode 100644 index 000000000..5a0b578e5 --- /dev/null +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.spec.ts @@ -0,0 +1,23 @@ +import { MonitoringPropertiesComponent } from './monitoring-properties-g.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +describe('MonitoringPropertiesGComponent', () => { + let component: MonitoringPropertiesComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [MonitoringPropertiesComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MonitoringPropertiesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts new file mode 100644 index 000000000..ee28aad16 --- /dev/null +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts @@ -0,0 +1,101 @@ +import { Component, EventEmitter, Input, OnInit, Output, SimpleChanges } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { Subscription } from 'rxjs'; + +import { TOOLTIPMESSAGEALERT } from '../../constants/guard'; +import { ISitesGroup } from '../../interfaces/geom'; +import { IobjObs, ObjDataType } from '../../interfaces/objObs'; +import { FormService } from '../../services/form.service'; +import { ObjectService } from '../../services/object.service'; +import { JsonData } from '../../types/jsondata'; +import { TPermission } from '../../types/permission'; +import { ObjectsPermissionMonitorings } from '../../enum/objectPermission'; +import { MediaService } from '@geonature_common/service/media.service'; + +@Component({ + selector: 'pnx-monitoring-properties-g', + templateUrl: './monitoring-properties-g.component.html', + styleUrls: ['./monitoring-properties-g.component.css'], +}) +export class MonitoringPropertiesGComponent implements OnInit { + // selectedObj: ISitesGroup; + @Input() selectedObj: ObjDataType; + @Input() selectedObjRaw: ObjDataType; + @Input() bEdit: boolean; + @Output() bEditChange = new EventEmitter(); + objectType: IobjObs; + + @Input() newParentType; + color: string = 'white'; + dataDetails: ISitesGroup; + fields: JsonData; + fieldDefinitions: JsonData = {}; + fieldsNames: string[]; + endPoint: string; + datasetForm = new FormControl(); + _sub: Subscription; + + specificFields: JsonData; + specificFieldDefinitions: JsonData = {}; + specificFieldsNames: string[]; + + @Input() permission: TPermission; + + canUpdateObj: boolean; + + toolTipNotAllowed: string; + + constructor( + private _formService: FormService, + private _objService: ObjectService, + public ms: MediaService + ) {} + + ngOnInit() { + this.toolTipNotAllowed = TOOLTIPMESSAGEALERT; + } + + initProperties() { + this.objectType = this.newParentType; + this.fieldsNames = this.newParentType.template.fieldNames; + this.fields = this.newParentType.template.fieldLabels; + this.fieldDefinitions = this.newParentType.template.fieldDefinitions; + this.objectType.properties = this.selectedObj; + this.endPoint = this.newParentType.endPoint; + } + + initSpecificProperties() { + // Suppression des propriétés génériques incluses dans les propriétés spécifiques + this.specificFieldsNames = this.newParentType.template_specific.fieldNames.filter( + (field) => !this.fieldsNames.includes(field) + ); + this.specificFields = this.newParentType.template_specific.fieldLabels; + this.specificFieldDefinitions = this.newParentType.template_specific.fieldDefinitions; + } + + onEditClick() { + this.selectedObjRaw['id'] = this.selectedObjRaw[this.selectedObjRaw.pk]; + this._formService.changeDataSub( + this.selectedObjRaw, + this.objectType.objectType, + this.objectType.endPoint + ); + this.bEditChange.emit(true); + } + + ngOnChanges(changes: SimpleChanges): void { + if (this.newParentType && this.newParentType.template.fieldNames.length != 0) { + this.initProperties(); + if (this.selectedObj) { + this.canUpdateObj = this.selectedObj['cruved']['U']; + } + if ( + this.newParentType.template_specific && + this.newParentType.template_specific.fieldNames && + this.newParentType.template_specific.fieldNames.length != 0 + ) { + this.initSpecificProperties(); + } + } + } +} diff --git a/frontend/app/components/monitoring-properties/monitoring-properties.component.css b/frontend/app/components/monitoring-properties/monitoring-properties.component.css index c2fc53c60..14aa0b367 100644 --- a/frontend/app/components/monitoring-properties/monitoring-properties.component.css +++ b/frontend/app/components/monitoring-properties/monitoring-properties.component.css @@ -26,8 +26,12 @@ td { display: none; } - ::ng-deep .cdk-global-overlay-wrapper, ::ng-deep .cdk-overlay-container { - z-index: 99999 !important; + z-index: 99999 !important; +} + +button:disabled { + cursor: not-allowed; + pointer-events: all !important; } diff --git a/frontend/app/components/monitoring-properties/monitoring-properties.component.html b/frontend/app/components/monitoring-properties/monitoring-properties.component.html index 667e8f286..fc9b3f97d 100644 --- a/frontend/app/components/monitoring-properties/monitoring-properties.component.html +++ b/frontend/app/components/monitoring-properties/monitoring-properties.component.html @@ -1,10 +1,10 @@
-
+
- - + + +
+ {{ obj.template.fieldLabels[fieldName] }} help {{ obj.resolvedProperties[fieldName] }}{{ obj.resolvedProperties[fieldName] }}
+
+ + +
+ + + +
+ {{ fieldName.value }} + help + {{ obj.resolvedProperties[fieldName.key] }}
{{ media.title_fr }} - ({{ ms.typeMedia(media) - }} par {{ media.author }} par {{ media.author }})
-

{{media.description_fr}}

+

{{ media.description_fr }}

- - + - + + mat-stroked-button + color="primary" + class="float-right" + (click)="updateSynthese()" + *ngIf=" + !bEdit && + obj.objectType === 'module' && + currentUser?.moduleCruved.module.U >= 1 && + obj.properties['b_synthese'] + " + matTooltip="Attention, cette opération peux prendre du temps" + > + - + Mettre à jour la synthèse + sync + +
- +