diff --git a/backend/geonature/core/gn_synthese/routes.py b/backend/geonature/core/gn_synthese/routes.py index 52916aea83..16e8dac2a1 100644 --- a/backend/geonature/core/gn_synthese/routes.py +++ b/backend/geonature/core/gn_synthese/routes.py @@ -531,16 +531,29 @@ def export_observations_web(permissions): POST parameters: Use a list of id_synthese (in POST parameters) to filter the v_synthese_for_export_view + :query str export_format: str<'csv', 'geojson', 'shapefiles', 'gpkg'> :query str export_format: str<'csv', 'geojson', 'shapefiles', 'gpkg'> """ params = request.args # set default to csv export_format = params.get("export_format", "csv") + view_name_param = params.get("view_name", "gn_synthese.v_synthese_for_export") # Test export_format - if not export_format in current_app.config["SYNTHESE"]["EXPORT_FORMAT"]: + if export_format not in current_app.config["SYNTHESE"]["EXPORT_FORMAT"]: raise BadRequest("Unsupported format") + # Test export view name is config params for security reason + if view_name_param != "gn_synthese.v_synthese_for_export" and view_name_param not in map( + lambda exp: exp["view_name"], + current_app.config["SYNTHESE"]["EXPORT_OBSERVATIONS_CUSTOM_VIEWS"], + ): + raise Forbidden("This view is not available for export") + try: + schema_name, view_name = view_name_param.split(".") + except ValueError: + raise BadRequest("view_name parameter must be a string with schema dot view_name") + # get list of id synthese from POST id_list = request.get_json() @@ -555,13 +568,16 @@ def export_observations_web(permissions): # Useful to have geom column so that they can be replaced by blurred geoms # (only if the user has sensitive permissions) export_view = GenericTableGeo( - tableName="v_synthese_for_export", - schemaName="gn_synthese", + tableName=view_name, + schemaName=schema_name, engine=DB.engine, geometry_field=None, srid=local_srid, ) + if "id_synthese" not in map(lambda col: col.name, export_view.db_cols): + raise BadRequest(f"The custom view {view_name} has not 'id_synthese' mandatory column") + # If there is no sensitive permissions => same path as before blurring implementation if not blurring_permissions: # Get the CTE for synthese filtered by user permissions diff --git a/backend/geonature/tests/test_synthese.py b/backend/geonature/tests/test_synthese.py index aff7627fe2..9c30af2047 100644 --- a/backend/geonature/tests/test_synthese.py +++ b/backend/geonature/tests/test_synthese.py @@ -564,6 +564,31 @@ def test_export(self, users): ) assert response.status_code == 200 + @pytest.mark.parametrize( + "view_name, response_status_code", + [ + ("gn_synthese.v_synthese_for_web_app", 200), + ("gn_synthese.not_in_config", 403), + ("v_synthese_for_web_app", 400), + ("gn_synthese.v_metadata_for_export", 400), + ], + ) + def test_export_observations_custom_view(self, users, app, view_name, response_status_code): + set_logged_user(self.client, users["self_user"]) + if view_name != "gn_synthese.not_in_config": + app.config["SYNTHESE"]["EXPORT_OBSERVATIONS_CUSTOM_VIEWS"] = [ + {"label": "Test export custom", "view_name": view_name} + ] + response = self.client.post( + url_for("gn_synthese.export_observations_web"), + json=[1, 2, 3], + query_string={ + "export_format": "geojson", + "view_name": view_name, + }, + ) + assert response.status_code == response_status_code + def test_export_observations(self, users, synthese_data, synthese_sensitive_data, modules): data_synthese = synthese_data.values() data_synthese_sensitive = synthese_sensitive_data.values() diff --git a/backend/geonature/utils/config_schema.py b/backend/geonature/utils/config_schema.py index 2dee8393f2..13a5220af3 100644 --- a/backend/geonature/utils/config_schema.py +++ b/backend/geonature/utils/config_schema.py @@ -281,6 +281,11 @@ class GnFrontEndConf(Schema): DISPLAY_EMAIL_DISPLAY_INFO = fields.List(fields.String(), load_default=["NOM_VERN"]) +class ExportObservationSchema(Schema): + label = fields.String() + view_name = fields.String() + + class Synthese(Schema): # -------------------------------------------------------------------- # SYNTHESE - SEARCH FORM @@ -364,6 +369,9 @@ class Synthese(Schema): # -------------------------------------------------------------------- # SYNTHESE - DOWNLOADS (AKA EXPORTS) EXPORT_COLUMNS = fields.List(fields.String(), load_default=DEFAULT_EXPORT_COLUMNS) + EXPORT_OBSERVATIONS_CUSTOM_VIEWS = fields.List( + fields.Nested(ExportObservationSchema), load_default=[] + ) # Certaines colonnes sont obligatoires pour effectuer les filtres CRUVED EXPORT_ID_SYNTHESE_COL = fields.String(load_default="id_synthese") EXPORT_ID_DATASET_COL = fields.String(load_default="jdd_id") diff --git a/config/default_config.toml.example b/config/default_config.toml.example index 3a6f1323b8..9e14b921a3 100644 --- a/config/default_config.toml.example +++ b/config/default_config.toml.example @@ -336,6 +336,10 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" # Formats d'export disponibles ["csv", "geojson", "shapefile", "gpkg"] EXPORT_FORMAT = ["csv", "geojson", "shapefile"] + # Vues d'export personnalisées + EXPORT_OBSERVATIONS_CUSTOM_VIEWS = [ + {"label" : "export customisé", "view_name"= "gn_synthese.ma_vue_personnalisee"} + ] # Noms des colonnes obligatoires de la vue ``gn_synthese.v_metadata_for_export`` EXPORT_METADATA_ID_DATASET_COL = "jdd_id" EXPORT_METADATA_ACTOR_COL = "acteurs" diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index be29b47200..7d6e0cc586 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,6 +7,8 @@ CHANGELOG - [Synthèse] Possibilité d'ajouter des champs supplémentaires à la liste de résultats via le paramètre `ADDITIONAL_COLUMNS_FRONTEND`. Ces champs sont masqués par défaut et controlables depuis l'interface (#2946) +- [Synthèse] Possibilité d'ajouter des exports customisés basé sur des vues SQL via le paramètre `EXPORT_OBSERVATIONS_CUSTOM_VIEWS` + 2.14.0 - Talpa europaea 👓 (2024-02-28) --------------------------------------- diff --git a/frontend/cypress/e2e/synthese-spec.js b/frontend/cypress/e2e/synthese-spec.js index dc9fe6a233..a868a9a937 100644 --- a/frontend/cypress/e2e/synthese-spec.js +++ b/frontend/cypress/e2e/synthese-spec.js @@ -193,7 +193,7 @@ describe('Tests gn_synthese', () => { // }); it('Should download data at the csv format', function () { - cy.intercept('POST', '/synthese/export_observations?export_format=csv').as('exportCall'); + cy.intercept('POST', '/synthese/export_observations?export_format=csv**').as('exportCall'); cy.get('[data-qa="synthese-download-btn"]').click(); cy.get('[data-qa="download-csv"]').click({ 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 f63db135a4..98fac70769 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 @@ -102,10 +102,10 @@ export class SyntheseDataService { return this._api.get(`${this.config.API_ENDPOINT}/synthese/taxons_tree`); } - downloadObservations(idSyntheseList: Array, format: string) { + downloadObservations(idSyntheseList: Array, format: string, view_name: string) { this.isDownloading = true; - const queryString = new HttpParams().set('export_format', format); - + let queryString = new HttpParams().set('export_format', format); + queryString = queryString.set('view_name', view_name); const source = this._api.post( `${this.config.API_ENDPOINT}/synthese/export_observations`, idSyntheseList, diff --git a/frontend/src/app/syntheseModule/synthese-results/synthese-list/modal-download/modal-download.component.html b/frontend/src/app/syntheseModule/synthese-results/synthese-list/modal-download/modal-download.component.html index c0010cd365..6ca6bed8f8 100644 --- a/frontend/src/app/syntheseModule/synthese-results/synthese-list/modal-download/modal-download.component.html +++ b/frontend/src/app/syntheseModule/synthese-results/synthese-list/modal-download/modal-download.component.html @@ -58,13 +58,13 @@
-
Télécharger les observations
+
Télécharger les observations - format standard
+
+
Télécharger les observations - {{ export.label }}
+ +
Télécharger les taxons
diff --git a/frontend/src/app/syntheseModule/synthese-results/synthese-list/modal-download/modal-download.component.ts b/frontend/src/app/syntheseModule/synthese-results/synthese-list/modal-download/modal-download.component.ts index 5d692757bc..54ae87fcb2 100644 --- a/frontend/src/app/syntheseModule/synthese-results/synthese-list/modal-download/modal-download.component.ts +++ b/frontend/src/app/syntheseModule/synthese-results/synthese-list/modal-download/modal-download.component.ts @@ -24,8 +24,8 @@ export class SyntheseModalDownloadComponent { this.syntheseConfig = this.config.SYNTHESE; } - downloadObservations(format) { - this._dataService.downloadObservations(this._storeService.idSyntheseList, format); + downloadObservations(format, view_name) { + this._dataService.downloadObservations(this._storeService.idSyntheseList, format, view_name); } downloadTaxons(format, filename) {