From 7fc66bb98f08a3084032763db902236592cecc05 Mon Sep 17 00:00:00 2001 From: Etienne Delclaux Date: Fri, 10 Jan 2025 17:08:29 +0100 Subject: [PATCH 01/13] feat: add 'ENABLE_TAXON_SHEETS' option in 'SYNTHESE' --- backend/geonature/core/gn_synthese/routes.py | 136 +++++++++--------- backend/geonature/utils/config_schema.py | 1 + config/default_config.toml.example | 2 + .../synthese-info-obs.component.html | 2 +- .../synthese-list.component.html | 2 +- .../src/app/syntheseModule/synthese.module.ts | 1 + .../taxon-sheet/taxon-sheet.route.service.ts | 13 +- 7 files changed, 88 insertions(+), 69 deletions(-) diff --git a/backend/geonature/core/gn_synthese/routes.py b/backend/geonature/core/gn_synthese/routes.py index e2dc80e61e..fc41bcf29d 100644 --- a/backend/geonature/core/gn_synthese/routes.py +++ b/backend/geonature/core/gn_synthese/routes.py @@ -82,6 +82,7 @@ VMTaxrefListForautocomplete, ) +from geonature import app routes = Blueprint("gn_synthese", __name__) @@ -957,81 +958,86 @@ def general_stats(permissions): } return data +## ############################################################################ +## TAXON SHEET ROUTES +## ############################################################################ -@routes.route("/taxon_stats/", methods=["GET"]) -@permissions.check_cruved_scope("R", get_scope=True, module_code="SYNTHESE") -@json_resp -def taxon_stats(scope, cd_nom): - """Return stats for a specific taxon""" +if app.config["SYNTHESE"]["ENABLE_TAXON_SHEETS"]: - area_type = request.args.get("area_type") + @routes.route("/taxon_stats/", methods=["GET"]) + @permissions.check_cruved_scope("R", get_scope=True, module_code="SYNTHESE") + @json_resp + def taxon_stats(scope, cd_nom): + """Return stats for a specific taxon""" - if not area_type: - raise BadRequest("Missing area_type parameter") + area_type = request.args.get("area_type") - # Ensure area_type is valid - valid_area_types = ( - db.session.query(BibAreasTypes.type_code) - .distinct() - .filter(BibAreasTypes.type_code == area_type) - .scalar() - ) - if not valid_area_types: - raise BadRequest("Invalid area_type") - - # Subquery to fetch areas based on area_type - areas_subquery = ( - select([LAreas.id_area]) - .where(LAreas.id_type == BibAreasTypes.id_type) - .where(BibAreasTypes.type_code == area_type) - .alias("areas") - ) - cd_ref = db.session.scalar(select(Taxref.cd_ref).where(Taxref.cd_nom == cd_nom)) - taxref_cd_nom_list = db.session.scalars(select(Taxref.cd_nom).where(Taxref.cd_ref == cd_ref)) + if not area_type: + raise BadRequest("Missing area_type parameter") - # Main query to fetch stats - query = ( - select( - [ - func.count(distinct(Synthese.id_synthese)).label("observation_count"), - func.count(distinct(Synthese.observers)).label("observer_count"), - func.count(distinct(areas_subquery.c.id_area)).label("area_count"), - func.min(Synthese.altitude_min).label("altitude_min"), - func.max(Synthese.altitude_max).label("altitude_max"), - func.min(Synthese.date_min).label("date_min"), - func.max(Synthese.date_max).label("date_max"), - ] + # Ensure area_type is valid + valid_area_types = ( + db.session.query(BibAreasTypes.type_code) + .distinct() + .filter(BibAreasTypes.type_code == area_type) + .scalar() ) - .select_from( - sa.join( - Synthese, - CorAreaSynthese, - Synthese.id_synthese == CorAreaSynthese.id_synthese, + if not valid_area_types: + raise BadRequest("Invalid area_type") + + # Subquery to fetch areas based on area_type + areas_subquery = ( + select([LAreas.id_area]) + .where(LAreas.id_type == BibAreasTypes.id_type) + .where(BibAreasTypes.type_code == area_type) + .alias("areas") + ) + cd_ref = db.session.scalar(select(Taxref.cd_ref).where(Taxref.cd_nom == cd_nom)) + taxref_cd_nom_list = db.session.scalars(select(Taxref.cd_nom).where(Taxref.cd_ref == cd_ref)) + + # Main query to fetch stats + query = ( + select( + [ + func.count(distinct(Synthese.id_synthese)).label("observation_count"), + func.count(distinct(Synthese.observers)).label("observer_count"), + func.count(distinct(areas_subquery.c.id_area)).label("area_count"), + func.min(Synthese.altitude_min).label("altitude_min"), + func.max(Synthese.altitude_max).label("altitude_max"), + func.min(Synthese.date_min).label("date_min"), + func.max(Synthese.date_max).label("date_max"), + ] ) - .join(areas_subquery, CorAreaSynthese.id_area == areas_subquery.c.id_area) - .join(LAreas, CorAreaSynthese.id_area == LAreas.id_area) - .join(BibAreasTypes, LAreas.id_type == BibAreasTypes.id_type) + .select_from( + sa.join( + Synthese, + CorAreaSynthese, + Synthese.id_synthese == CorAreaSynthese.id_synthese, + ) + .join(areas_subquery, CorAreaSynthese.id_area == areas_subquery.c.id_area) + .join(LAreas, CorAreaSynthese.id_area == LAreas.id_area) + .join(BibAreasTypes, LAreas.id_type == BibAreasTypes.id_type) + ) + .where(Synthese.cd_nom.in_(taxref_cd_nom_list)) ) - .where(Synthese.cd_nom.in_(taxref_cd_nom_list)) - ) - synthese_query_obj = SyntheseQuery(Synthese, query, {}) - synthese_query_obj.filter_query_with_cruved(g.current_user, scope) - result = DB.session.execute(synthese_query_obj.query) - synthese_stats = result.fetchone() - - data = { - "cd_ref": cd_nom, - "observation_count": synthese_stats["observation_count"], - "observer_count": synthese_stats["observer_count"], - "area_count": synthese_stats["area_count"], - "altitude_min": synthese_stats["altitude_min"], - "altitude_max": synthese_stats["altitude_max"], - "date_min": synthese_stats["date_min"], - "date_max": synthese_stats["date_max"], - } + synthese_query_obj = SyntheseQuery(Synthese, query, {}) + synthese_query_obj.filter_query_with_cruved(g.current_user, scope) + result = DB.session.execute(synthese_query_obj.query) + synthese_stats = result.fetchone() + + data = { + "cd_ref": cd_nom, + "observation_count": synthese_stats["observation_count"], + "observer_count": synthese_stats["observer_count"], + "area_count": synthese_stats["area_count"], + "altitude_min": synthese_stats["altitude_min"], + "altitude_max": synthese_stats["altitude_max"], + "date_min": synthese_stats["date_min"], + "date_max": synthese_stats["date_max"], + } - return data + return data @routes.route("/taxons_tree", methods=["GET"]) diff --git a/backend/geonature/utils/config_schema.py b/backend/geonature/utils/config_schema.py index 4d21312bba..8babab638d 100644 --- a/backend/geonature/utils/config_schema.py +++ b/backend/geonature/utils/config_schema.py @@ -439,6 +439,7 @@ class Synthese(Schema): # -------------------------------------------------------------------- # SYNTHESE - TAXON_SHEET + ENABLE_TAXON_SHEETS = fields.Boolean(load_default=True) TAXON_SHEET = fields.Nested(TaxonSheet, load_default=TaxonSheet().load({})) @pre_load diff --git a/config/default_config.toml.example b/config/default_config.toml.example index c6a2e164c6..97c827756e 100644 --- a/config/default_config.toml.example +++ b/config/default_config.toml.example @@ -441,6 +441,8 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" # Seulement les données de présence cd_nomenclature_observation_status = ['Pr'] + # Activer l'affichage des informations liées à la fiche taxon dans la synthèse + ENABLE_TAXON_SHEETS = true [SYNTHESE.TAXON_SHEET] # Options dédiées à la fiche taxon # Permet d'activer ou non l'onglet "Profil" diff --git a/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/synthese-info-obs.component.html b/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/synthese-info-obs.component.html index 5e0dfbcef3..876dfde2c8 100644 --- a/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/synthese-info-obs.component.html +++ b/frontend/src/app/shared/syntheseSharedModule/synthese-info-obs/synthese-info-obs.component.html @@ -191,7 +191,7 @@

diff --git a/frontend/src/app/syntheseModule/synthese.module.ts b/frontend/src/app/syntheseModule/synthese.module.ts index 43c2f6cded..46baba7032 100644 --- a/frontend/src/app/syntheseModule/synthese.module.ts +++ b/frontend/src/app/syntheseModule/synthese.module.ts @@ -41,6 +41,7 @@ const routes: Routes = [ { path: 'taxon/:cd_ref', component: TaxonSheetComponent, + canActivate: [RouteService], canActivateChild: [RouteService], children: [ { diff --git a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts index 8676c6bb89..1170541485 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts +++ b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts @@ -4,6 +4,7 @@ import { RouterStateSnapshot, Router, CanActivateChild, + CanActivate } from '@angular/router'; import { ConfigService } from '@geonature/services/config.service'; import { Observable } from 'rxjs'; @@ -42,7 +43,7 @@ export const ALL_TAXON_SHEET_ADVANCED_INFOS_ROUTES: Array = [ @Injectable({ providedIn: 'root', }) -export class RouteService implements CanActivateChild { +export class RouteService implements CanActivate, CanActivateChild { readonly TAB_LINKS = []; constructor( private _config: ConfigService, @@ -55,11 +56,19 @@ export class RouteService implements CanActivateChild { ); } } + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { + if(!this._config.SYNTHESE.ENABLE_TAXON_SHEETS){ + this._router.navigate(['/404'], { skipLocationChange: true }); + return false; + } + + return true; + } canActivateChild( childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot - ): Observable | Promise | boolean { + ): boolean { const targetedPath = childRoute.routeConfig.path; if (this.TAB_LINKS.map((tab) => tab.path).includes(targetedPath)) { return true; From 31d73534bb9d5ca671ca772bd131e66a1725c690 Mon Sep 17 00:00:00 2001 From: Etienne Delclaux Date: Fri, 10 Jan 2025 17:16:15 +0100 Subject: [PATCH 02/13] feat: ENABLE_XXX to ENABLE_TAB_XXX for taxon sheet tab --- backend/geonature/utils/config_schema.py | 4 ++-- config/default_config.toml.example | 4 ++-- .../syntheseModule/taxon-sheet/taxon-sheet.route.service.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/geonature/utils/config_schema.py b/backend/geonature/utils/config_schema.py index 8babab638d..5bfbcad425 100644 --- a/backend/geonature/utils/config_schema.py +++ b/backend/geonature/utils/config_schema.py @@ -279,8 +279,8 @@ class ExportObservationSchema(Schema): class TaxonSheet(Schema): # -------------------------------------------------------------------- # SYNTHESE - TAXON_SHEET - ENABLE_PROFILE = fields.Boolean(load_default=True) - ENABLE_TAXONOMY = fields.Boolean(load_default=True) + ENABLE_TAB_PROFILES = fields.Boolean(load_default=True) + ENABLE_TAB_TAXONOMY = fields.Boolean(load_default=True) class Synthese(Schema): diff --git a/config/default_config.toml.example b/config/default_config.toml.example index 97c827756e..9c68545ee9 100644 --- a/config/default_config.toml.example +++ b/config/default_config.toml.example @@ -446,9 +446,9 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" [SYNTHESE.TAXON_SHEET] # Options dédiées à la fiche taxon # Permet d'activer ou non l'onglet "Profil" - ENABLE_PROFILE = true + ENABLE_TAB_PROFILES = true # Permet d'activer ou non l'onglet "Taxonomie" - ENABLE_TAXONOMY = true + ENABLE_TAB_TAXONOMY = true # Gestion des demandes d'inscription [ACCOUNT_MANAGEMENT] diff --git a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts index 1170541485..3064ecaf0d 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts +++ b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts @@ -29,13 +29,13 @@ export const ALL_TAXON_SHEET_ADVANCED_INFOS_ROUTES: Array = [ { label: 'Taxonomie', path: 'taxonomy', - configEnabledField: 'ENABLE_TAXONOMY', + configEnabledField: 'ENABLE_TAB_TAXONOMY', component: TabTaxonomyComponent, }, { label: 'Profil', path: 'profile', - configEnabledField: 'ENABLE_PROFILE', + configEnabledField: 'ENABLE_TAB_PROFILES', component: TabProfileComponent, }, ]; From a57d0bc3fd2e45e0275f9f113329b450ab84423c Mon Sep 17 00:00:00 2001 From: Etienne Delclaux Date: Fri, 10 Jan 2025 17:42:07 +0100 Subject: [PATCH 03/13] feat: add a relation between 'FRONTEND.ENABLE_PROFILES' and 'SYNTHESE.TAXON_SHEET.ENABLE_TAB_PROFILES' --- backend/geonature/utils/config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/geonature/utils/config.py b/backend/geonature/utils/config.py index 34acffb504..912b73aadb 100644 --- a/backend/geonature/utils/config.py +++ b/backend/geonature/utils/config.py @@ -33,6 +33,11 @@ except ValidationError as e: raise ConfigError(CONFIG_FILE, e.messages) +# Handle consistency inside the frontend config + +if(config_frontend["SYNTHESE"]["TAXON_SHEET"]["ENABLE_TAB_PROFILES"] and not config_frontend["FRONTEND"]["ENABLE_PROFILES"]): + config_frontend["SYNTHESE"]["TAXON_SHEET"]["ENABLE_TAB_PROFILES"] = False + config_default = { # disable cache for downloaded files (PDF file stat for ex) # TODO: use Flask.get_send_file_max_age(filename) to return 0 only for generated PDF files From 9cf9a35f0c38fffbb524908ab14983916bbe1411 Mon Sep 17 00:00:00 2001 From: Etienne Delclaux Date: Fri, 10 Jan 2025 17:43:40 +0100 Subject: [PATCH 04/13] fix: ENABLE_TAB_PROFILES --> ENABLE_TAB_PROFILE --- backend/geonature/utils/config_schema.py | 2 +- config/default_config.toml.example | 14 +++++++------- .../taxon-sheet/taxon-sheet.route.service.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/geonature/utils/config_schema.py b/backend/geonature/utils/config_schema.py index 5bfbcad425..acf325eab2 100644 --- a/backend/geonature/utils/config_schema.py +++ b/backend/geonature/utils/config_schema.py @@ -279,7 +279,7 @@ class ExportObservationSchema(Schema): class TaxonSheet(Schema): # -------------------------------------------------------------------- # SYNTHESE - TAXON_SHEET - ENABLE_TAB_PROFILES = fields.Boolean(load_default=True) + ENABLE_TAB_PROFILE = fields.Boolean(load_default=True) ENABLE_TAB_TAXONOMY = fields.Boolean(load_default=True) diff --git a/config/default_config.toml.example b/config/default_config.toml.example index 9c68545ee9..773c0d2459 100644 --- a/config/default_config.toml.example +++ b/config/default_config.toml.example @@ -446,7 +446,7 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" [SYNTHESE.TAXON_SHEET] # Options dédiées à la fiche taxon # Permet d'activer ou non l'onglet "Profil" - ENABLE_TAB_PROFILES = true + ENABLE_TAB_PROFILE = true # Permet d'activer ou non l'onglet "Taxonomie" ENABLE_TAB_TAXONOMY = true @@ -625,8 +625,8 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" # Encodage des fichiers importés autorisées ENCODAGE = ["UTF-8"] - # Bounding box des données de l'instance. - # Utilisé pour lever des warning lorsque les données sont en dehors. + # Bounding box des données de l'instance. + # Utilisé pour lever des warning lorsque les données sont en dehors. # Format: [XMIN, YMIN, XMAX, YMAX] # Par défaut: France métropolitaine incluant la Corse INSTANCE_BOUNDING_BOX = [-5.0, 41.0, 10.0, 51.15] @@ -645,7 +645,7 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" # SRID autorisés pour les fichiers en entrée SRID = [ - {name = "WGS84", code = 4326}, + {name = "WGS84", code = 4326}, {name = "Lambert93", code = 2154} ] # Extensions autorisées (seul le csv est accepté actuellement) @@ -657,7 +657,7 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" # Si le mapping des valeurs est désactivé, specifier l'identifiant du mapping qui doit être utilisé DEFAULT_VALUE_MAPPING_ID = 3 - # rempli les valeurs de nomenclature erroné par la valeur par defaut + # rempli les valeurs de nomenclature erroné par la valeur par defaut # Leve un warning et non une erreur sur les lignes concernées FILL_MISSING_NOMENCLATURE_WITH_DEFAULT_VALUE = false @@ -678,7 +678,7 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" # Customiser le nom du fichier de rapport de l'import # Pour indiquer des données liés à l'import dans le nom du fichier ajouter le nom de la variable - # contenant cette dernière. Les variables suivantes sont accessibles : + # contenant cette dernière. Les variables suivantes sont accessibles : # - date_create_import -> date de création de l'import # - dataset.dataset_name -> nom du jeu de données de destination # - dataset.active -> Si le jeu de données de destination est actif @@ -705,7 +705,7 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" # Id d'une liste de taxons permettant de restreindre l'import d'observations de taxons comprises dans cette dernière - # Lève une exception si un taxon n'appartenant pas à liste indiquée apparaît dans les donnés importées. + # Lève une exception si un taxon n'appartenant pas à liste indiquée apparaît dans les donnés importées. ID_LIST_TAXA_RESTRICTION = fields.Integer(load_default=None) # URL d'accès au module d'import diff --git a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts index 3064ecaf0d..358800f474 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts +++ b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts @@ -35,7 +35,7 @@ export const ALL_TAXON_SHEET_ADVANCED_INFOS_ROUTES: Array = [ { label: 'Profil', path: 'profile', - configEnabledField: 'ENABLE_TAB_PROFILES', + configEnabledField: 'ENABLE_TAB_PROFILE', component: TabProfileComponent, }, ]; From ed90d5045c564f9c36d94ca2827e133c62a5a539 Mon Sep 17 00:00:00 2001 From: Etienne Delclaux Date: Fri, 10 Jan 2025 17:49:07 +0100 Subject: [PATCH 05/13] lint --- backend/geonature/core/gn_synthese/routes.py | 5 ++++- backend/geonature/utils/config.py | 5 ++++- .../synthese-list/synthese-list.component.html | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/geonature/core/gn_synthese/routes.py b/backend/geonature/core/gn_synthese/routes.py index fc41bcf29d..6ef882d551 100644 --- a/backend/geonature/core/gn_synthese/routes.py +++ b/backend/geonature/core/gn_synthese/routes.py @@ -958,6 +958,7 @@ def general_stats(permissions): } return data + ## ############################################################################ ## TAXON SHEET ROUTES ## ############################################################################ @@ -993,7 +994,9 @@ def taxon_stats(scope, cd_nom): .alias("areas") ) cd_ref = db.session.scalar(select(Taxref.cd_ref).where(Taxref.cd_nom == cd_nom)) - taxref_cd_nom_list = db.session.scalars(select(Taxref.cd_nom).where(Taxref.cd_ref == cd_ref)) + taxref_cd_nom_list = db.session.scalars( + select(Taxref.cd_nom).where(Taxref.cd_ref == cd_ref) + ) # Main query to fetch stats query = ( diff --git a/backend/geonature/utils/config.py b/backend/geonature/utils/config.py index 912b73aadb..cd28de3bce 100644 --- a/backend/geonature/utils/config.py +++ b/backend/geonature/utils/config.py @@ -35,7 +35,10 @@ # Handle consistency inside the frontend config -if(config_frontend["SYNTHESE"]["TAXON_SHEET"]["ENABLE_TAB_PROFILES"] and not config_frontend["FRONTEND"]["ENABLE_PROFILES"]): +if ( + config_frontend["SYNTHESE"]["TAXON_SHEET"]["ENABLE_TAB_PROFILES"] + and not config_frontend["FRONTEND"]["ENABLE_PROFILES"] +): config_frontend["SYNTHESE"]["TAXON_SHEET"]["ENABLE_TAB_PROFILES"] = False config_default = { diff --git a/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-list.component.html b/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-list.component.html index e8d3aa8ba2..0c66e81c05 100644 --- a/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-list.component.html +++ b/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-list.component.html @@ -93,7 +93,10 @@ From eafc120f1ef231364ed7bceabe57324c7a21f40e Mon Sep 17 00:00:00 2001 From: jacquesfize Date: Mon, 13 Jan 2025 10:21:55 +0100 Subject: [PATCH 06/13] feat(config): profile display config coherence moved to config_schema --- backend/geonature/utils/config.py | 6 ------ backend/geonature/utils/config_schema.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/backend/geonature/utils/config.py b/backend/geonature/utils/config.py index cd28de3bce..27236f0f77 100644 --- a/backend/geonature/utils/config.py +++ b/backend/geonature/utils/config.py @@ -35,12 +35,6 @@ # Handle consistency inside the frontend config -if ( - config_frontend["SYNTHESE"]["TAXON_SHEET"]["ENABLE_TAB_PROFILES"] - and not config_frontend["FRONTEND"]["ENABLE_PROFILES"] -): - config_frontend["SYNTHESE"]["TAXON_SHEET"]["ENABLE_TAB_PROFILES"] = False - config_default = { # disable cache for downloaded files (PDF file stat for ex) # TODO: use Flask.get_send_file_max_age(filename) to return 0 only for generated PDF files diff --git a/backend/geonature/utils/config_schema.py b/backend/geonature/utils/config_schema.py index acf325eab2..d6c9574263 100644 --- a/backend/geonature/utils/config_schema.py +++ b/backend/geonature/utils/config_schema.py @@ -613,3 +613,13 @@ def insert_module_config(self, data, **kwargs): continue data[module_code] = get_module_config(dist) return data + + @post_load + def profile_display_coherence(self, data, **kwargs): + if ( + data["SYNTHESE"]["TAXON_SHEET"]["ENABLE_TAB_PROFILE"] + and not data["FRONTEND"]["ENABLE_PROFILES"] + ): + data["SYNTHESE"]["TAXON_SHEET"]["ENABLE_TAB_PROFILE"] = False + + return data From 17678ab8a47bf9e94a88f0fdfd6adad3c04a8815 Mon Sep 17 00:00:00 2001 From: Etienne Delclaux <150020787+edelclaux@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:48:00 +0100 Subject: [PATCH 07/13] Remove obsolete comment Co-authored-by: Jacques Fize <4259846+jacquesfize@users.noreply.github.com> --- backend/geonature/utils/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/geonature/utils/config.py b/backend/geonature/utils/config.py index 27236f0f77..34acffb504 100644 --- a/backend/geonature/utils/config.py +++ b/backend/geonature/utils/config.py @@ -33,8 +33,6 @@ except ValidationError as e: raise ConfigError(CONFIG_FILE, e.messages) -# Handle consistency inside the frontend config - config_default = { # disable cache for downloaded files (PDF file stat for ex) # TODO: use Flask.get_send_file_max_age(filename) to return 0 only for generated PDF files From a956d35159c5467da5a9d7ad9ae9729487da6a37 Mon Sep 17 00:00:00 2001 From: Etienne Delclaux Date: Tue, 14 Jan 2025 16:08:30 +0100 Subject: [PATCH 08/13] feat: add test for config processing --- backend/geonature/tests/test_utils.py | 68 ++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/backend/geonature/tests/test_utils.py b/backend/geonature/tests/test_utils.py index f3055e97da..0e4559d292 100644 --- a/backend/geonature/tests/test_utils.py +++ b/backend/geonature/tests/test_utils.py @@ -8,10 +8,14 @@ from marshmallow.exceptions import ValidationError +############################################################################# +# BASIC TEMPLATE CONFIG FILE +############################################################################# + TEMPLATE_CONFIG_FILE = """ SQLALCHEMY_DATABASE_URI = "postgresql://monuser:monpassachanger@localhost:5432/mabase" URL_APPLICATION = 'http://url.com/geonature' -API_ENDPOINT = 'http://url.com/geonature/api' +API_ENDPOINT = 'http://url.com/geonature/api' SECRET_KEY = 'super secret key' @@ -37,6 +41,44 @@ [MEDIAS] """ +############################################################################# +# TAXON SHEET CONFIG FILE +############################################################################# + +TEMPLATE_TAXON_SHEET_CONFIG_FILE = """ + SQLALCHEMY_DATABASE_URI = "postgresql://monuser:monpassachanger@localhost:5432/mabase" + URL_APPLICATION = 'http://url.com/geonature' + API_ENDPOINT = 'http://url.com/geonature/api' + + SECRET_KEY = 'super secret key' + + DEFAULT_LANGUAGE=fr + [HOME] + TITLE = "Bienvenue dans GeoNature" + INTRODUCTION = "Texte d'introduction, configurable pour le modifier régulièrement ou le masquer" + FOOTER = "" + + # Configuration liée aux ID de BDD + [BDD] + + # Configuration générale du frontend + [FRONTEND] + ENABLE_PROFILES={ENABLE_PROFILES} + + # Configuration de la Synthese + [SYNTHESE] + ENABLE_TAXON_SHEETS={ENABLE_TAXON_SHEETS} + [SYNTHESE.TAXON_SHEET] + ENABLE_TAB_TAXONOMY={ENABLE_TAB_TAXONOMY} + ENABLE_TAB_PROFILE={ENABLE_TAB_PROFILE} + + # Configuration cartographique + [MAPCONFIG] + + # Configuration médias + [MEDIAS] + """ + @pytest.mark.usefixtures("temporary_transaction") class TestUtils: @@ -59,3 +101,27 @@ def test_utilstoml(self): with pytest.raises(ConfigError): load_and_validate_toml(f.name, GnPySchemaConf) + + @pytest.mark.parametrize( + "enable_profiles,enable_tab_profile,expected_enable_tab_profile", + [(True, True, True), (True, False, False), (False, False, False), (False, True, False)], + ) + def test_config_profiles_consistency( + self, enable_profiles, enable_tab_profile, expected_enable_tab_profile + ): + + profiles_config = TEMPLATE_TAXON_SHEET_CONFIG_FILE.format( + ENABLE_TAXON_SHEETS=True, + ENABLE_TAB_TAXONOMY=True, + ENABLE_PROFILES=enable_profiles, + ENABLE_TAB_PROFILE=enable_tab_profile, + ) + + with tempfile.NamedTemporaryFile(mode="w") as f: + f.write(profiles_config) + with pytest.raises(ConfigError): + config = load_and_validate_toml(f.name, GnPySchemaConf) + assert ( + config["SYNTHESE"]["TAXON_SHEET"]["ENABLE_TAB_PROFILE"] + == expected_enable_tab_profile + ) From cf2b79c8d99cef24dbc9be064b50d9de4553d400 Mon Sep 17 00:00:00 2001 From: Etienne Delclaux Date: Tue, 14 Jan 2025 16:15:20 +0100 Subject: [PATCH 09/13] fix: apply suggestion on id_area --- backend/geonature/core/gn_synthese/routes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/geonature/core/gn_synthese/routes.py b/backend/geonature/core/gn_synthese/routes.py index 6ef882d551..f871f090e3 100644 --- a/backend/geonature/core/gn_synthese/routes.py +++ b/backend/geonature/core/gn_synthese/routes.py @@ -988,9 +988,8 @@ def taxon_stats(scope, cd_nom): # Subquery to fetch areas based on area_type areas_subquery = ( - select([LAreas.id_area]) - .where(LAreas.id_type == BibAreasTypes.id_type) - .where(BibAreasTypes.type_code == area_type) + select(LAreas.id_area) + .where(LAreas.id_type == BibAreasTypes.id_type, BibAreasTypes.type_code == area_type) .alias("areas") ) cd_ref = db.session.scalar(select(Taxref.cd_ref).where(Taxref.cd_nom == cd_nom)) From cf0a1a678fabc633a2975ccca873d38df55c85a0 Mon Sep 17 00:00:00 2001 From: Etienne Delclaux Date: Tue, 14 Jan 2025 16:17:35 +0100 Subject: [PATCH 10/13] feat: squash commit for observers --- backend/geonature/core/gn_synthese/routes.py | 94 ++++++++++---- .../core/gn_synthese/utils/taxon_sheet.py | 64 +++++++++ backend/geonature/tests/fixtures.py | 13 +- backend/geonature/tests/test_synthese.py | 122 +++++++++++++++++- backend/geonature/utils/config_schema.py | 3 + config/default_config.toml.example | 14 +- .../synthese-data-pagination-item.ts | 11 ++ .../synthese-form/synthese-data-sort-item.ts | 14 ++ .../synthese-form/synthese-data.service.ts | 17 +++ .../syntheseModule/taxon-sheet/loadable.ts | 19 +++ .../tab-observers.component.html | 106 +++++++++++++++ .../tab-observers.component.scss | 5 + .../tab-observers/tab-observers.component.ts | 95 ++++++++++++++ .../tab-profile/tab-profile.component.ts | 2 +- .../taxon-sheet/taxon-sheet.route.service.ts | 7 + 15 files changed, 546 insertions(+), 40 deletions(-) create mode 100644 backend/geonature/core/gn_synthese/utils/taxon_sheet.py create mode 100644 frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data-pagination-item.ts create mode 100644 frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data-sort-item.ts create mode 100644 frontend/src/app/syntheseModule/taxon-sheet/loadable.ts create mode 100644 frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.html create mode 100644 frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.scss create mode 100644 frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.ts diff --git a/backend/geonature/core/gn_synthese/routes.py b/backend/geonature/core/gn_synthese/routes.py index e2dc80e61e..b8de33c4bd 100644 --- a/backend/geonature/core/gn_synthese/routes.py +++ b/backend/geonature/core/gn_synthese/routes.py @@ -22,7 +22,7 @@ from werkzeug.exceptions import Forbidden, NotFound, BadRequest, Conflict from werkzeug.datastructures import MultiDict from sqlalchemy import distinct, func, desc, asc, select, case, or_ -from sqlalchemy.orm import joinedload, lazyload, selectinload, contains_eager, raiseload +from sqlalchemy.orm import joinedload, lazyload, selectinload, contains_eager, raiseload, Query from geojson import FeatureCollection, Feature import sqlalchemy as sa from sqlalchemy.orm import load_only, aliased, Load, with_expression @@ -44,6 +44,7 @@ from geonature.core.gn_synthese.models import ( BibReportsTypes, CorAreaSynthese, + CorObserverSynthese, DefaultsNomenclaturesValue, Synthese, TSources, @@ -52,7 +53,12 @@ TReport, SyntheseLogEntry, ) +from geonature.core.gn_commons.models import TMedias + +from pypnusershub.db import User + from geonature.core.gn_synthese.synthese_config import MANDATORY_COLUMNS +from geonature.core.gn_synthese.utils.taxon_sheet import TaxonSheetUtils, SortOrder from geonature.core.gn_synthese.utils.blurring import ( build_allowed_geom_cte, @@ -67,7 +73,6 @@ from geonature.core.gn_permissions.decorators import login_required, permissions_required from geonature.core.gn_permissions.tools import get_scopes_by_action, get_permissions from geonature.core.sensitivity.models import cor_sensitivity_area_type - from ref_geo.models import LAreas, BibAreasTypes from apptax.taxonomie.models import ( @@ -82,6 +87,8 @@ VMTaxrefListForautocomplete, ) +from geonature import app + routes = Blueprint("gn_synthese", __name__) @@ -958,10 +965,10 @@ def general_stats(permissions): return data -@routes.route("/taxon_stats/", methods=["GET"]) +@routes.route("/taxon_stats/", methods=["GET"]) @permissions.check_cruved_scope("R", get_scope=True, module_code="SYNTHESE") @json_resp -def taxon_stats(scope, cd_nom): +def taxon_stats(scope, cd_ref): """Return stats for a specific taxon""" area_type = request.args.get("area_type") @@ -969,25 +976,11 @@ def taxon_stats(scope, cd_nom): if not area_type: raise BadRequest("Missing area_type parameter") - # Ensure area_type is valid - valid_area_types = ( - db.session.query(BibAreasTypes.type_code) - .distinct() - .filter(BibAreasTypes.type_code == area_type) - .scalar() - ) - if not valid_area_types: - raise BadRequest("Invalid area_type") - - # Subquery to fetch areas based on area_type - areas_subquery = ( - select([LAreas.id_area]) - .where(LAreas.id_type == BibAreasTypes.id_type) - .where(BibAreasTypes.type_code == area_type) - .alias("areas") - ) - cd_ref = db.session.scalar(select(Taxref.cd_ref).where(Taxref.cd_nom == cd_nom)) - taxref_cd_nom_list = db.session.scalars(select(Taxref.cd_nom).where(Taxref.cd_ref == cd_ref)) + if not TaxonSheetUtils.is_valid_area_type(area_type): + raise BadRequest("Invalid area_type parameter") + + areas_subquery = TaxonSheetUtils.get_area_subquery(area_type) + taxref_cd_nom_list = TaxonSheetUtils.get_cd_nom_list_from_cd_ref(cd_ref) # Main query to fetch stats query = ( @@ -1015,13 +1008,12 @@ def taxon_stats(scope, cd_nom): .where(Synthese.cd_nom.in_(taxref_cd_nom_list)) ) - synthese_query_obj = SyntheseQuery(Synthese, query, {}) - synthese_query_obj.filter_query_with_cruved(g.current_user, scope) - result = DB.session.execute(synthese_query_obj.query) + synthese_query = TaxonSheetUtils.get_synthese_query_with_scope(g.current_user, scope, query) + result = DB.session.execute(synthese_query) synthese_stats = result.fetchone() data = { - "cd_ref": cd_nom, + "cd_ref": cd_ref, "observation_count": synthese_stats["observation_count"], "observer_count": synthese_stats["observer_count"], "area_count": synthese_stats["area_count"], @@ -1034,6 +1026,54 @@ def taxon_stats(scope, cd_nom): return data +if app.config["SYNTHESE"]["TAXON_SHEET"]["ENABLE_TAB_OBSERVERS"]: + + @routes.route("/taxon_observers/", methods=["GET"]) + @permissions.check_cruved_scope("R", get_scope=True, module_code="SYNTHESE") + # @json_resp + def taxon_observers(scope, cd_ref): + per_page = request.args.get("per_page", 10, int) + page = request.args.get("page", 1, int) + sort_by = request.args.get("sort_by", "observer") + sort_order = request.args.get("sort_order", SortOrder.ASC, SortOrder) + field_separator = request.args.get( + "field_separator", app.config["SYNTHESE"]["FIELD_OBSERVERS_SEPARATOR"] + ) + + # Handle sorting + if sort_by not in ["observer", "date_min", "date_max", "observation_count", "media_count"]: + raise BadRequest(f"The sort_by column {sort_by} is not defined") + + taxref_cd_nom_list = TaxonSheetUtils.get_cd_nom_list_from_cd_ref(cd_ref) + + query = ( + db.session.query( + func.trim( + func.unnest(func.string_to_array(Synthese.observers, field_separator)) + ).label("observer"), + func.min(Synthese.date_min).label("date_min"), + func.max(Synthese.date_max).label("date_max"), + func.count(Synthese.id_synthese).label("observation_count"), + func.count(TMedias.id_media).label("media_count"), + ) + .group_by("observer") + .outerjoin(Synthese.medias) + .where(Synthese.cd_nom.in_(taxref_cd_nom_list)) + ) + query = TaxonSheetUtils.get_synthese_query_with_scope(g.current_user, scope, query) + query = TaxonSheetUtils.update_query_with_sorting(query, sort_by, sort_order) + results = TaxonSheetUtils.paginate(query, page, per_page) + + return jsonify( + { + "items": results.items, + "total": results.total, + "per_page": per_page, + "page": page, + } + ) + + @routes.route("/taxons_tree", methods=["GET"]) @login_required @json_resp diff --git a/backend/geonature/core/gn_synthese/utils/taxon_sheet.py b/backend/geonature/core/gn_synthese/utils/taxon_sheet.py new file mode 100644 index 0000000000..373b4e4a8f --- /dev/null +++ b/backend/geonature/core/gn_synthese/utils/taxon_sheet.py @@ -0,0 +1,64 @@ +import typing +from geonature.utils.env import db +from ref_geo.models import LAreas, BibAreasTypes + +from geonature.core.gn_synthese.models import Synthese +from sqlalchemy import select, desc, asc +from apptax.taxonomie.models import Taxref +from geonature.core.gn_synthese.utils.query_select_sqla import SyntheseQuery +from sqlalchemy.orm import Query +from werkzeug.exceptions import BadRequest +from flask_sqlalchemy.pagination import Pagination +from enum import Enum + + +class SortOrder(Enum): + ASC = "asc" + DESC = "desc" + + +class TaxonSheetUtils: + + @staticmethod + def update_query_with_sorting(query: Query, sort_by: str, sort_order: SortOrder) -> Query: + if sort_order == SortOrder.ASC: + return query.order_by(asc(sort_by)) + + return query.order_by(desc(sort_by)) + + @staticmethod + def paginate(query: Query, page: int, per_page: int) -> Pagination: + return query.paginate(page=page, per_page=per_page, error_out=False) + + # + @staticmethod + def get_cd_nom_list_from_cd_ref(cd_ref: int) -> typing.List[int]: + return db.session.scalars(select(Taxref.cd_nom).where(Taxref.cd_ref == cd_ref)) + + @staticmethod + def get_synthese_query_with_scope(current_user, scope: int, query: Query) -> SyntheseQuery: + synthese_query_obj = SyntheseQuery(Synthese, query, {}) + synthese_query_obj.filter_query_with_cruved(current_user, scope) + return synthese_query_obj.query + + @staticmethod + def is_valid_area_type(area_type: str) -> bool: + # Ensure area_type is valid + valid_area_types = ( + db.session.query(BibAreasTypes.type_code) + .distinct() + .filter(BibAreasTypes.type_code == area_type) + .scalar() + ) + + return valid_area_types + + @staticmethod + def get_area_subquery(area_type: str) -> Query: + + # Subquery to fetch areas based on area_type + return ( + select(LAreas.id_area) + .where(LAreas.id_type == BibAreasTypes.id_type, BibAreasTypes.type_code == area_type) + .alias("areas") + ) diff --git a/backend/geonature/tests/fixtures.py b/backend/geonature/tests/fixtures.py index fba219ad84..91b41b593b 100644 --- a/backend/geonature/tests/fixtures.py +++ b/backend/geonature/tests/fixtures.py @@ -330,12 +330,12 @@ def create_user( db.session.add(organisme) users_to_create = [ - (("noright_user", organisme, 0), {}), - (("stranger_user", None, 2), {}), - (("associate_user", organisme, 2), {}), - (("self_user", organisme, 1), {}), + (("noright_user", organisme, 0), {"nom_role": "User", "prenom_role": "NoRight"}), + (("stranger_user", None, 2), {"nom_role": "User", "prenom_role": "Stranger"}), + (("associate_user", organisme, 2), {"nom_role": "User", "prenom_role": "Associate"}), + (("self_user", organisme, 1), {"nom_role": "User", "prenom_role": "Self"}), (("user", organisme, 2), {"nom_role": "Bob", "prenom_role": "Bobby"}), - (("admin_user", organisme, 3), {}), + (("admin_user", organisme, 3), {"nom_role": "Administrateur", "prenom_role": "Test"}), (("associate_user_2_exclude_sensitive", organisme, 2, True), {}), ( ( @@ -546,6 +546,7 @@ def create_synthese( source, uuid=func.uuid_generate_v4(), cor_observers=[], + observers=[], date_min="", date_max="", altitude_min=800, @@ -574,6 +575,7 @@ def create_synthese( altitude_min=altitude_min, altitude_max=altitude_max, cor_observers=cor_observers, + observers=observers, **kwargs, ) @@ -743,6 +745,7 @@ def synthese_data(app, users, datasets, source, sources_modules): source_m, unique_id_sinp, [users["admin_user"], users["user"]], + ["Administrative Test", "Bobby Bob"], date_min, date_max, altitude_min, diff --git a/backend/geonature/tests/test_synthese.py b/backend/geonature/tests/test_synthese.py index f78116a4bb..3ff15da3e5 100644 --- a/backend/geonature/tests/test_synthese.py +++ b/backend/geonature/tests/test_synthese.py @@ -1154,7 +1154,7 @@ def test_taxon_stats(self, synthese_data, users): url_for("gn_synthese.taxon_stats", cd_nom=CD_REF_VALID, area_type=AREA_TYPE_INVALID), ) assert response.status_code == 400 - assert response.json["description"] == "Invalid area_type" + assert response.json["description"] == "Invalid area_type parameter" # Invalid cd_ref parameter response = self.client.get( @@ -1225,6 +1225,126 @@ def test_get_one_synthese_record(self, app, users, synthese_data): ) assert response.status_code == Forbidden.code + def test_taxon_observer(self, synthese_data, users): + set_logged_user(self.client, users["stranger_user"]) + + ## Test Data + + SORT_ORDER_UNDEFINED = "sort-order-undefined" + SORT_ORDER_ASC = "asc" + SORT_ORDER_DESC = "desc" + PER_PAGE = 2 + SORT_BY_UNDEFINED = "sort-by-undefined" + + CD_REF = 2497 + CD_REF_OBSERVERS_ASC = { + "items": [ + { + "date_max": "Thu, 03 Oct 2024 08:09:10 GMT", + "date_min": "Wed, 02 Oct 2024 11:22:33 GMT", + "media_count": 0, + "observation_count": 3, + "observer": "Administrateur Test", + }, + { + "date_max": "Thu, 03 Oct 2024 08:09:10 GMT", + "date_min": "Wed, 02 Oct 2024 11:22:33 GMT", + "media_count": 0, + "observation_count": 3, + "observer": "Bob Bobby", + }, + ], + "page": 1, + "per_page": 2, + "total": 2, + } + CD_REF_OBSERVERS_DESC = { + "items": [ + { + "date_max": "Thu, 03 Oct 2024 08:09:10 GMT", + "date_min": "Wed, 02 Oct 2024 11:22:33 GMT", + "media_count": 0, + "observation_count": 3, + "observer": "Bob Bobby", + }, + { + "date_max": "Thu, 03 Oct 2024 08:09:10 GMT", + "date_min": "Wed, 02 Oct 2024 11:22:33 GMT", + "media_count": 0, + "observation_count": 3, + "observer": "Administrateur Test", + }, + ], + "page": 1, + "per_page": 2, + "total": 2, + } + + ## sort_order + + # Unknow sort_order parameters: shoudl fallback in asc + response = self.client.get( + url_for( + "gn_synthese.taxon_observers", + cd_ref=CD_REF, + per_page=PER_PAGE, + sort_order=SORT_ORDER_UNDEFINED, + ), + ) + assert response.status_code == 200 + assert response.get_json() == CD_REF_OBSERVERS_ASC + + # sort order ASC + response = self.client.get( + url_for( + "gn_synthese.taxon_observers", + cd_ref=CD_REF, + per_page=PER_PAGE, + sort_order=SORT_ORDER_ASC, + ), + ) + assert response.status_code == 200 + assert response.get_json() == CD_REF_OBSERVERS_ASC + + # sort order DESC + response = self.client.get( + url_for( + "gn_synthese.taxon_observers", + cd_ref=CD_REF, + per_page=PER_PAGE, + sort_order=SORT_ORDER_DESC, + ), + ) + assert response.status_code == 200 + assert response.get_json() == CD_REF_OBSERVERS_DESC + + ## sort_by + response = self.client.get( + url_for( + "gn_synthese.taxon_observers", + cd_ref=CD_REF, + per_page=PER_PAGE, + sort_order=SORT_ORDER_ASC, + sort_by=SORT_BY_UNDEFINED, + ), + ) + assert response.status_code == BadRequest.code + assert ( + response.json["description"] == f"The sort_by column {SORT_BY_UNDEFINED} is not defined" + ) + + # Ok + response = self.client.get( + url_for( + "gn_synthese.taxon_observers", + cd_ref=CD_REF, + per_page=PER_PAGE, + ) + ) + + assert response.status_code == 200 + assert response.get_json() == CD_REF_OBSERVERS_ASC + def test_color_taxon(self, synthese_data, users): # Note: require grids 5×5! set_logged_user(self.client, users["self_user"]) diff --git a/backend/geonature/utils/config_schema.py b/backend/geonature/utils/config_schema.py index 4d21312bba..f731091d66 100644 --- a/backend/geonature/utils/config_schema.py +++ b/backend/geonature/utils/config_schema.py @@ -281,6 +281,7 @@ class TaxonSheet(Schema): # SYNTHESE - TAXON_SHEET ENABLE_PROFILE = fields.Boolean(load_default=True) ENABLE_TAXONOMY = fields.Boolean(load_default=True) + ENABLE_TAB_OBSERVERS = fields.Boolean(load_default=True) class Synthese(Schema): @@ -441,6 +442,8 @@ class Synthese(Schema): # SYNTHESE - TAXON_SHEET TAXON_SHEET = fields.Nested(TaxonSheet, load_default=TaxonSheet().load({})) + FIELD_OBSERVERS_SEPARATOR = fields.String(load_default=",") + @pre_load def warn_deprecated(self, data, **kwargs): deprecated = { diff --git a/config/default_config.toml.example b/config/default_config.toml.example index c6a2e164c6..897f2a4030 100644 --- a/config/default_config.toml.example +++ b/config/default_config.toml.example @@ -447,6 +447,8 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" ENABLE_PROFILE = true # Permet d'activer ou non l'onglet "Taxonomie" ENABLE_TAXONOMY = true + # Permet d'activer ou non la section "Observers" + ENABLE_TAB_OBSERVERS = true # Gestion des demandes d'inscription [ACCOUNT_MANAGEMENT] @@ -623,8 +625,8 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" # Encodage des fichiers importés autorisées ENCODAGE = ["UTF-8"] - # Bounding box des données de l'instance. - # Utilisé pour lever des warning lorsque les données sont en dehors. + # Bounding box des données de l'instance. + # Utilisé pour lever des warning lorsque les données sont en dehors. # Format: [XMIN, YMIN, XMAX, YMAX] # Par défaut: France métropolitaine incluant la Corse INSTANCE_BOUNDING_BOX = [-5.0, 41.0, 10.0, 51.15] @@ -643,7 +645,7 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" # SRID autorisés pour les fichiers en entrée SRID = [ - {name = "WGS84", code = 4326}, + {name = "WGS84", code = 4326}, {name = "Lambert93", code = 2154} ] # Extensions autorisées (seul le csv est accepté actuellement) @@ -655,7 +657,7 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" # Si le mapping des valeurs est désactivé, specifier l'identifiant du mapping qui doit être utilisé DEFAULT_VALUE_MAPPING_ID = 3 - # rempli les valeurs de nomenclature erroné par la valeur par defaut + # rempli les valeurs de nomenclature erroné par la valeur par defaut # Leve un warning et non une erreur sur les lignes concernées FILL_MISSING_NOMENCLATURE_WITH_DEFAULT_VALUE = false @@ -676,7 +678,7 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" # Customiser le nom du fichier de rapport de l'import # Pour indiquer des données liés à l'import dans le nom du fichier ajouter le nom de la variable - # contenant cette dernière. Les variables suivantes sont accessibles : + # contenant cette dernière. Les variables suivantes sont accessibles : # - date_create_import -> date de création de l'import # - dataset.dataset_name -> nom du jeu de données de destination # - dataset.active -> Si le jeu de données de destination est actif @@ -703,7 +705,7 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" # Id d'une liste de taxons permettant de restreindre l'import d'observations de taxons comprises dans cette dernière - # Lève une exception si un taxon n'appartenant pas à liste indiquée apparaît dans les donnés importées. + # Lève une exception si un taxon n'appartenant pas à liste indiquée apparaît dans les donnés importées. ID_LIST_TAXA_RESTRICTION = fields.Integer(load_default=None) # URL d'accès au module d'import diff --git a/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data-pagination-item.ts b/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data-pagination-item.ts new file mode 100644 index 0000000000..07726a73ae --- /dev/null +++ b/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data-pagination-item.ts @@ -0,0 +1,11 @@ +export interface SyntheseDataPaginationItem { + totalItems: number; + currentPage: number; + perPage: number; +} + +export const DEFAULT_PAGINATION: SyntheseDataPaginationItem = { + totalItems: 0, + currentPage: 1, + perPage: 10, +}; diff --git a/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data-sort-item.ts b/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data-sort-item.ts new file mode 100644 index 0000000000..e11b73f9d6 --- /dev/null +++ b/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data-sort-item.ts @@ -0,0 +1,14 @@ +export enum SORT_ORDER { + ASC = 'asc', + DESC = 'desc', +} + +export interface SyntheseDataSortItem { + sortBy: string; + sortOrder: string; +} + +export const DEFAULT_SORT: SyntheseDataSortItem = { + sortBy: '', + sortOrder: SORT_ORDER.ASC, +}; diff --git a/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data.service.ts b/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data.service.ts index 78a2f064f5..c3d6d4f549 100644 --- a/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data.service.ts +++ b/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data.service.ts @@ -11,6 +11,8 @@ import { BehaviorSubject } from 'rxjs'; import { CommonService } from '@geonature_common/service/common.service'; import { Observable } from 'rxjs'; import { ConfigService } from '@geonature/services/config.service'; +import { DEFAULT_PAGINATION, SyntheseDataPaginationItem } from './synthese-data-pagination-item'; +import { DEFAULT_SORT, SyntheseDataSortItem } from './synthese-data-sort-item'; export const FormatMapMime = new Map([ ['csv', 'text/csv'], @@ -61,6 +63,21 @@ export class SyntheseDataService { }); } + getSyntheseTaxonSheetObservers( + cd_ref: number, + pagination: SyntheseDataPaginationItem = DEFAULT_PAGINATION, + sort: SyntheseDataSortItem = DEFAULT_SORT + ) { + return this._api.get(`${this.config.API_ENDPOINT}/synthese/taxon_observers/${cd_ref}`, { + params: { + per_page: pagination.perPage, + page: pagination.currentPage, + sort_by: sort.sortBy, + sort_order: sort.sortOrder, + }, + }); + } + getTaxaCount(params = {}) { let queryString = new HttpParams(); for (let key in params) { diff --git a/frontend/src/app/syntheseModule/taxon-sheet/loadable.ts b/frontend/src/app/syntheseModule/taxon-sheet/loadable.ts new file mode 100644 index 0000000000..4561510d9b --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/loadable.ts @@ -0,0 +1,19 @@ +export class Loadable { + _isLoading: boolean; + + constructor(isLoading: boolean = false) { + this._isLoading = isLoading; + } + + startLoading() { + this._isLoading = true; + } + + stopLoading() { + this._isLoading = false; + } + + get isLoading(): boolean { + return this._isLoading; + } +} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.html b/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.html new file mode 100644 index 0000000000..5547e2e902 --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.html @@ -0,0 +1,106 @@ +
+ + {{ renderDate(date) }} + + + + {{ value ? value : '-' }} + + + + {{ value }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.scss b/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.scss new file mode 100644 index 0000000000..2e38c350ec --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.scss @@ -0,0 +1,5 @@ +.Observers { + &__table { + box-shadow: none; + } +} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.ts b/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.ts new file mode 100644 index 0000000000..301f1f175a --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.ts @@ -0,0 +1,95 @@ +import { Component, OnInit } from '@angular/core'; +import { GN2CommonModule } from '@geonature_common/GN2Common.module'; +import { CommonModule } from '@angular/common'; +import { ConfigService } from '@geonature/services/config.service'; +import { Taxon } from '@geonature_common/form/taxonomy/taxonomy.component'; +import { TaxonSheetService } from '../taxon-sheet.service'; +import { SyntheseDataService } from '@geonature_common/form/synthese-form/synthese-data.service'; +import { + DEFAULT_PAGINATION, + SyntheseDataPaginationItem, +} from '@geonature_common/form/synthese-form/synthese-data-pagination-item'; +import { + DEFAULT_SORT, + SORT_ORDER, + SyntheseDataSortItem, +} from '@geonature_common/form/synthese-form/synthese-data-sort-item'; +import { Loadable } from '../loadable'; +import { finalize } from 'rxjs/operators'; +@Component({ + standalone: true, + selector: 'tab-observers', + templateUrl: 'tab-observers.component.html', + styleUrls: ['tab-observers.component.scss'], + imports: [GN2CommonModule, CommonModule], +}) +export class TabObserversComponent extends Loadable implements OnInit { + readonly PROP_OBSERVER = 'observer'; + readonly PROP_DATE_MIN = 'date_min'; + readonly PROP_DATE_MAX = 'date_max'; + readonly PROP_OBSERVATION_COUNT = 'observation_count'; + readonly PROP_MEDIA_COUNT = 'media_count'; + + readonly DEFAULT_SORT = { + ...DEFAULT_SORT, + sortBy: this.PROP_OBSERVER, + sortOrder: SORT_ORDER.ASC, + }; + items: any[] = []; + pagination: SyntheseDataPaginationItem = DEFAULT_PAGINATION; + sort: SyntheseDataSortItem = this.DEFAULT_SORT; + + constructor( + private _syntheseDataService: SyntheseDataService, + private _tss: TaxonSheetService + ) { + super(); + } + + ngOnInit() { + this._tss.taxon.subscribe((taxon: Taxon | null) => { + this.fetchObservers(); + }); + } + + renderDate(date: string): string { + return new Date(date).toLocaleDateString(); + } + + onChangePage(event) { + this.pagination.currentPage = event.offset + 1; + this.fetchObservers(); + } + + onSort(event) { + this.sort = { + sortBy: event.column.prop, + sortOrder: event.newValue, + }; + this.fetchObservers(); + } + + async fetchObservers() { + this.startLoading(); + const taxon = this._tss.taxon.getValue(); + this.items = []; + if (!taxon) { + this.pagination = DEFAULT_PAGINATION; + this.sort = this.DEFAULT_SORT; + this.stopLoading(); + return; + } + this._syntheseDataService + .getSyntheseTaxonSheetObservers(taxon.cd_ref, this.pagination, this.sort) + .pipe(finalize(() => this.stopLoading())) + .subscribe((data) => { + // Store result + this.items = data.items; + this.pagination = { + totalItems: data.total, + currentPage: data.page, + perPage: data.per_page, + }; + }); + } +} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/tab-profile/tab-profile.component.ts b/frontend/src/app/syntheseModule/taxon-sheet/tab-profile/tab-profile.component.ts index 56a044c0e6..6e328de5f5 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/tab-profile/tab-profile.component.ts +++ b/frontend/src/app/syntheseModule/taxon-sheet/tab-profile/tab-profile.component.ts @@ -72,7 +72,7 @@ export class TabProfileComponent implements OnInit { (errors) => { this.profile = null; if (errors.status == 404) { - this._commonService.regularToaster('warning', 'Aucune donnée pour ce taxon'); + this._commonService.regularToaster('warning', 'Aucune donnée de profil pour ce taxon'); } } ); diff --git a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts index 8676c6bb89..a1b6ea14c2 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts +++ b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts @@ -10,6 +10,7 @@ import { Observable } from 'rxjs'; import { TabGeographicOverviewComponent } from './tab-geographic-overview/tab-geographic-overview.component'; import { TabProfileComponent } from './tab-profile/tab-profile.component'; import { TabTaxonomyComponent } from './tab-taxonomy/tab-taxonomy.component'; +import { TabObserversComponent } from './tab-observers/tab-observers.component'; interface Tab { label: string; @@ -37,6 +38,12 @@ export const ALL_TAXON_SHEET_ADVANCED_INFOS_ROUTES: Array = [ configEnabledField: 'ENABLE_PROFILE', component: TabProfileComponent, }, + { + label: 'Observateurs', + path: 'observers', + configEnabledField: 'ENABLE_TAB_OBSERVERS', + component: TabObserversComponent, + }, ]; @Injectable({ From 8aa1aed5a487a76598ca18cd5eecc42bd107a054 Mon Sep 17 00:00:00 2001 From: Etienne Delclaux Date: Tue, 14 Jan 2025 17:50:31 +0100 Subject: [PATCH 11/13] feat: adjust test to cd_nom --> cd_ref --- backend/geonature/tests/test_synthese.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/geonature/tests/test_synthese.py b/backend/geonature/tests/test_synthese.py index 3ff15da3e5..cb4fd3e7c6 100644 --- a/backend/geonature/tests/test_synthese.py +++ b/backend/geonature/tests/test_synthese.py @@ -1144,28 +1144,28 @@ def test_taxon_stats(self, synthese_data, users): # Missing area_type parameter response = self.client.get( - url_for("gn_synthese.taxon_stats", cd_nom=CD_REF_VALID), + url_for("gn_synthese.taxon_stats", cd_ref=CD_REF_VALID), ) assert response.status_code == 400 assert response.json["description"] == "Missing area_type parameter" # Invalid area_type parameter response = self.client.get( - url_for("gn_synthese.taxon_stats", cd_nom=CD_REF_VALID, area_type=AREA_TYPE_INVALID), + url_for("gn_synthese.taxon_stats", cd_ref=CD_REF_VALID, area_type=AREA_TYPE_INVALID), ) assert response.status_code == 400 assert response.json["description"] == "Invalid area_type parameter" # Invalid cd_ref parameter response = self.client.get( - url_for("gn_synthese.taxon_stats", cd_nom=CD_REF_INVALID, area_type=AREA_TYPE_VALID), + url_for("gn_synthese.taxon_stats", cd_ref=CD_REF_INVALID, area_type=AREA_TYPE_VALID), ) assert response.status_code == 200 assert response.get_json() == CD_REF_INVALID_STATS # Invalid cd_ref parameter response = self.client.get( - url_for("gn_synthese.taxon_stats", cd_nom=CD_REF_VALID, area_type=AREA_TYPE_VALID), + url_for("gn_synthese.taxon_stats", cd_ref=CD_REF_VALID, area_type=AREA_TYPE_VALID), ) response_json = response.get_json() assert response.status_code == 200 From 04240f5090eb2bb770ade556aba6630bae5fd893 Mon Sep 17 00:00:00 2001 From: Etienne Delclaux Date: Wed, 15 Jan 2025 18:03:54 +0100 Subject: [PATCH 12/13] fix: remove @json_resp --- backend/geonature/core/gn_synthese/routes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/geonature/core/gn_synthese/routes.py b/backend/geonature/core/gn_synthese/routes.py index d7e0196d3a..105f358a9d 100644 --- a/backend/geonature/core/gn_synthese/routes.py +++ b/backend/geonature/core/gn_synthese/routes.py @@ -1041,7 +1041,6 @@ def taxon_stats(scope, cd_ref): @routes.route("/taxon_observers/", methods=["GET"]) @permissions.check_cruved_scope("R", get_scope=True, module_code="SYNTHESE") - @json_resp def taxon_observers(scope, cd_ref): per_page = request.args.get("per_page", 10, int) page = request.args.get("page", 1, int) From 1b1d22fc68ba2a1b7d27e2eb6bfaaf18b1ce5e0b Mon Sep 17 00:00:00 2001 From: Etienne Delclaux Date: Wed, 15 Jan 2025 18:22:11 +0100 Subject: [PATCH 13/13] feat: use SyntehseDataPagination/Sort in home discussions --- .../home-discussions-table.component.html | 2 +- .../home-discussions-table.component.ts | 36 ++++++++----------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/frontend/src/app/components/home-content/home-discussions/home-discussions-table/home-discussions-table.component.html b/frontend/src/app/components/home-content/home-discussions/home-discussions-table/home-discussions-table.component.html index 284659b63b..06fa7e9211 100644 --- a/frontend/src/app/components/home-content/home-discussions/home-discussions-table/home-discussions-table.component.html +++ b/frontend/src/app/components/home-content/home-discussions/home-discussions-table/home-discussions-table.component.html @@ -10,7 +10,7 @@ [count]="pagination.totalItems" (page)="onChangePage($event)" [externalSorting]="true" - [sorts]="[{ prop: sort.sort, dir: sort.orderby }]" + [sorts]="[{ prop: sort.sortBy, dir: sort.sortOrder }]" (sort)="onColumnSort($event)" > (); @@ -78,8 +72,8 @@ export class HomeDiscussionsTableComponent implements OnInit, OnDestroy { onColumnSort(event: any) { this.sort = { - sort: event.newValue, - orderby: event.column.prop, + sortBy: event.newValue, + sortOrder: event.column.prop, }; this.pagination.currentPage = 1; this._fetchDiscussions(); @@ -106,8 +100,8 @@ export class HomeDiscussionsTableComponent implements OnInit, OnDestroy { private _buildQueryParams(): URLSearchParams { const params = new URLSearchParams(); params.set('type', 'discussion'); - params.set('sort', this.sort.sort); - params.set('orderby', this.sort.orderby); + params.set('sort', this.sort.sortOrder); + params.set('orderby', this.sort.sortBy); params.set('page', this.pagination.currentPage.toString()); params.set('per_page', this.pagination.perPage.toString()); params.set('my_reports', this._myReportsOnly.toString());