diff --git a/backend/geonature/core/gn_synthese/models.py b/backend/geonature/core/gn_synthese/models.py index 4fa2ebd5da..76619542e3 100644 --- a/backend/geonature/core/gn_synthese/models.py +++ b/backend/geonature/core/gn_synthese/models.py @@ -586,7 +586,7 @@ class VSyntheseForWebApp(DB.Model): id_module = DB.Column(DB.Integer) entity_source_pk_value = DB.Column(DB.Integer) id_dataset = DB.Column(DB.Integer) - dataset_name = DB.Column(DB.Integer) + dataset_name = DB.Column(DB.String) id_acquisition_framework = DB.Column(DB.Integer) count_min = DB.Column(DB.Integer) count_max = DB.Column(DB.Integer) diff --git a/backend/geonature/core/gn_synthese/routes.py b/backend/geonature/core/gn_synthese/routes.py index 213b041b56..f9323c77da 100644 --- a/backend/geonature/core/gn_synthese/routes.py +++ b/backend/geonature/core/gn_synthese/routes.py @@ -16,6 +16,7 @@ g, ) from geonature.core.gn_synthese.schemas import SyntheseSchema +from geonature.core.gn_synthese.synthese_config import MANDATORY_COLUMNS from pypnusershub.db.models import User from pypnnomenclature.models import BibNomenclaturesTypes, TNomenclatures from werkzeug.exceptions import Forbidden, NotFound, BadRequest, Conflict @@ -151,12 +152,9 @@ def get_observations_for_web(permissions): col["prop"] for col in current_app.config["SYNTHESE"]["LIST_COLUMNS_FRONTEND"] } # Init with compulsory columns - columns = [ - "id", - VSyntheseForWebApp.id_synthese, - "url_source", - VSyntheseForWebApp.url_source, - ] + columns = [] + for col in MANDATORY_COLUMNS: + columns.extend([col, getattr(VSyntheseForWebApp, col)]) if "count_min_max" in param_column_list: count_min_max = case( diff --git a/backend/geonature/core/gn_synthese/schemas.py b/backend/geonature/core/gn_synthese/schemas.py index 2a317bf938..deea848cfa 100644 --- a/backend/geonature/core/gn_synthese/schemas.py +++ b/backend/geonature/core/gn_synthese/schemas.py @@ -1,7 +1,15 @@ from geonature.utils.env import db, ma +from geonature.utils.config import config from geonature.core.gn_commons.schemas import ModuleSchema, MediaSchema, TValidationSchema -from geonature.core.gn_synthese.models import BibReportsTypes, TReport, TSources, Synthese +from geonature.core.gn_synthese.models import ( + BibReportsTypes, + TReport, + TSources, + Synthese, + VSyntheseForWebApp, +) +from geonature.core.gn_synthese.synthese_config import MANDATORY_COLUMNS from pypn_habref_api.schemas import HabrefSchema from pypnusershub.schemas import UserSchema diff --git a/backend/geonature/core/gn_synthese/synthese_config.py b/backend/geonature/core/gn_synthese/synthese_config.py index 84caecfcc6..a3129d8d1b 100644 --- a/backend/geonature/core/gn_synthese/synthese_config.py +++ b/backend/geonature/core/gn_synthese/synthese_config.py @@ -91,7 +91,7 @@ ] # Mandatory columns for the frontend in Synthese API -MANDATORY_COLUMNS = ["entity_source_pk_value", "url_source", "cd_nom"] +MANDATORY_COLUMNS = ["id_synthese", "entity_source_pk_value", "url_source", "cd_nom"] # CONFIG MAP-LIST DEFAULT_LIST_COLUMN = [ diff --git a/backend/geonature/tests/test_synthese.py b/backend/geonature/tests/test_synthese.py index 5595d73cb3..90752076e6 100644 --- a/backend/geonature/tests/test_synthese.py +++ b/backend/geonature/tests/test_synthese.py @@ -14,16 +14,21 @@ from geoalchemy2.shape import to_shape, from_shape from shapely.testing import assert_geometries_equal from shapely.geometry import Point -from marshmallow import EXCLUDE +from marshmallow import EXCLUDE, fields, Schema +from marshmallow_geojson import FeatureSchema, GeoJSONSchema + from geonature.utils.env import db +from geonature.utils.config import config from geonature.core.gn_permissions.tools import get_permissions from geonature.core.gn_synthese.utils.blurring import split_blurring_precise_permissions +from geonature.core.gn_synthese.schemas import SyntheseSchema from geonature.core.gn_synthese.utils.query_select_sqla import remove_accents from geonature.core.sensitivity.models import cor_sensitivity_area_type from geonature.core.gn_meta.models import TDatasets from geonature.core.gn_synthese.models import Synthese, TSources, VSyntheseForWebApp -from geonature.core.gn_synthese.schemas import SyntheseSchema +from geonature.core.gn_synthese.synthese_config import MANDATORY_COLUMNS + from geonature.core.gn_permissions.models import PermAction, Permission from geonature.core.gn_commons.models.base import TModules @@ -32,9 +37,10 @@ from apptax.tests.fixtures import noms_example, attribut_example from pypnusershub.tests.utils import logged_user_headers, set_logged_user +from utils_flask_sqla_geo.schema import GeoModelConverter, GeoAlchemyAutoSchema + from .fixtures import * from .fixtures import create_synthese, create_module, synthese_with_protected_status -from .utils import jsonschema_definitions @pytest.fixture() @@ -106,59 +112,79 @@ def synthese_for_observers(source, datasets): ) -synthese_properties = { - "type": "object", - "properties": { - "observations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": {"type": "number"}, - "cd_nom": {"type": "number"}, - "count_min_max": {"type": "string"}, - "dataset_name": {"type": "string"}, - "date_min": {"type": "string"}, - "entity_source_pk_value": { - "oneOf": [ - {"type": "null"}, - {"type": "string"}, - ], - }, - "lb_nom": {"type": "string"}, - "nom_vern_or_lb_nom": {"type": "string"}, - "unique_id_sinp": { - "type": "string", - "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", - }, - "observers": { - "oneOf": [ - {"type": "null"}, - {"type": "string"}, - ], - }, - "url_source": { - "oneOf": [ - {"type": "null"}, - {"type": "string"}, - ], - }, - }, - "required": [ # obligatoire pour le fonctionement du front - "id", - "cd_nom", - "url_source", - "entity_source_pk_value", - ], - # "additionalProperties": False, - }, - }, - }, -} +# TODO : move and use those schemas in routes one day ! +class CustomRequiredConverter(GeoModelConverter): + """Custom converter to add kwargs required for mandatory and asked fields in get_observations_for_web view + Use to validate response in test""" + + def _add_column_kwargs(self, kwargs, column): + super()._add_column_kwargs(kwargs, column) + default_cols = map(lambda col: col["prop"], config["SYNTHESE"]["LIST_COLUMNS_FRONTEND"]) + required_cols = list(default_cols) + MANDATORY_COLUMNS + kwargs["required"] = column.name in required_cols + + +class VSyntheseForWebAppSchema(GeoAlchemyAutoSchema): + """ + Schema for serialization/deserialization of VSyntheseForWebApp class + """ + + count_min_max = fields.Str() + nom_vern_or_lb_nom = fields.Str() + + class Meta: + model = VSyntheseForWebApp + feature_geometry = "the_geom_4326" + sqla_session = db.session + model_converter = CustomRequiredConverter + + +# utility classes for VSyntheseForWebAppSchema validation +class UngroupedFeatureSchema(FeatureSchema): + properties = fields.Nested( + VSyntheseForWebAppSchema, + required=True, + ) + + +class GroupedFeatureSchema(FeatureSchema): + class NestedObs(Schema): + observations = fields.List( + fields.Nested(VSyntheseForWebAppSchema, required=True), required=True + ) + + properties = fields.Nested(NestedObs, required=True) + + +class UngroupedGeoJSONSchema(GeoJSONSchema): + feature_schema = UngroupedFeatureSchema + + +class GroupedGeoJSONSchema(GeoJSONSchema): + feature_schema = GroupedFeatureSchema @pytest.mark.usefixtures("client_class", "temporary_transaction") class TestSynthese: + def test_required_fields_and_format(self, app, users): + # Test required fields base on VSyntheseForWebAppSchema surrounded by a custom converter : CustomRequiredConverter + # also test geojson serialization (grouped by geometry and not) + app.config["SYNTHESE"]["LIST_COLUMNS_FRONTEND"] += [ + {"prop": "altitude_min", "name": "Altitude min"}, + {"prop": "count_min_max", "name": "Dénombrement"}, + {"prop": "nom_vern_or_lb_nom", "name": "Taxon"}, + ] + url_ungrouped = url_for("gn_synthese.get_observations_for_web") + set_logged_user(self.client, users["admin_user"]) + resp = self.client.get(url_ungrouped) + for f in resp.json["features"]: + UngroupedGeoJSONSchema().load(f) + + url_grouped = url_for("gn_synthese.get_observations_for_web", format="grouped_geom") + resp = self.client.get(url_grouped) + for f in resp.json["features"]: + GroupedGeoJSONSchema().load(f) + def test_synthese_scope_filtering(self, app, users, synthese_data): all_ids = {s.id_synthese for s in synthese_data.values()} sq = ( @@ -184,12 +210,6 @@ def test_get_defaut_nomenclatures(self, users): def test_get_observations_for_web(self, app, users, synthese_data, taxon_attribut): url = url_for("gn_synthese.get_observations_for_web") - schema = { - "definitions": jsonschema_definitions, - "$ref": "#/definitions/featurecollection", - "$defs": {"props": synthese_properties}, - } - r = self.client.get(url) assert r.status_code == Unauthorized.code @@ -197,7 +217,9 @@ def test_get_observations_for_web(self, app, users, synthese_data, taxon_attribu r = self.client.get(url) assert r.status_code == 200 - validate_json(instance=r.json, schema=schema) + + r = self.client.get(url) + assert r.status_code == 200 # Add cd_nom column app.config["SYNTHESE"]["LIST_COLUMNS_FRONTEND"] += [ @@ -215,7 +237,6 @@ def test_get_observations_for_web(self, app, users, synthese_data, taxon_attribu } r = self.client.post(url, json=filters) assert r.status_code == 200 - validate_json(instance=r.json, schema=schema) assert len(r.json["features"]) > 0 for feature in r.json["features"]: assert feature["properties"]["cd_nom"] == taxon_attribut.bib_nom.cd_nom @@ -241,12 +262,11 @@ def test_get_observations_for_web(self, app, users, synthese_data, taxon_attribu } r = self.client.post(url, json=filters) assert r.status_code == 200 - validate_json(instance=r.json, schema=schema) assert {synthese_data[k].id_synthese for k in ["p1_af1", "p1_af2"]}.issubset( - {f["properties"]["id"] for f in r.json["features"]} + {f["properties"]["id_synthese"] for f in r.json["features"]} ) assert {synthese_data[k].id_synthese for k in ["p2_af1", "p2_af2"]}.isdisjoint( - {f["properties"]["id"] for f in r.json["features"]} + {f["properties"]["id_synthese"] for f in r.json["features"]} ) # test geometry filter with circle radius @@ -264,12 +284,11 @@ def test_get_observations_for_web(self, app, users, synthese_data, taxon_attribu } r = self.client.post(url, json=filters) assert r.status_code == 200 - validate_json(instance=r.json, schema=schema) assert {synthese_data[k].id_synthese for k in ["p1_af1", "p1_af2"]}.issubset( - {f["properties"]["id"] for f in r.json["features"]} + {f["properties"]["id_synthese"] for f in r.json["features"]} ) assert {synthese_data[k].id_synthese for k in ["p2_af1", "p2_af2"]}.isdisjoint( - {f["properties"]["id"] for f in r.json["features"]} + {f["properties"]["id_synthese"] for f in r.json["features"]} ) # test ref geo area filter @@ -280,12 +299,11 @@ def test_get_observations_for_web(self, app, users, synthese_data, taxon_attribu filters = {f"area_{com_type.id_type}": [chambery.id_area]} r = self.client.post(url, json=filters) assert r.status_code == 200 - validate_json(instance=r.json, schema=schema) assert {synthese_data[k].id_synthese for k in ["p1_af1", "p1_af2"]}.issubset( - {f["properties"]["id"] for f in r.json["features"]} + {f["properties"]["id_synthese"] for f in r.json["features"]} ) assert {synthese_data[k].id_synthese for k in ["p2_af1", "p2_af2"]}.isdisjoint( - {f["properties"]["id"] for f in r.json["features"]} + {f["properties"]["id_synthese"] for f in r.json["features"]} ) # test organism @@ -294,7 +312,6 @@ def test_get_observations_for_web(self, app, users, synthese_data, taxon_attribu } r = self.client.post(url, json=filters) assert r.status_code == 200 - validate_json(instance=r.json, schema=schema) assert len(r.json["features"]) >= 2 # FIXME # test status lr @@ -343,7 +360,9 @@ def test_get_observations_for_web_filter_comment(self, users, synthese_data, tax filters = {"has_comment": True} r = self.client.get(url, json=filters) - assert id_synthese in (feature["properties"]["id"] for feature in r.json["features"]) + assert id_synthese in ( + feature["properties"]["id_synthese"] for feature in r.json["features"] + ) def test_get_observations_for_web_filter_id_source(self, users, synthese_data, source): set_logged_user(self.client, users["self_user"]) @@ -358,7 +377,7 @@ def test_get_observations_for_web_filter_id_source(self, users, synthese_data, s for synthese in synthese_data.values() if synthese.id_source == id_source } - response_data = {feature["properties"]["id"] for feature in r.json["features"]} + response_data = {feature["properties"]["id_synthese"] for feature in r.json["features"]} assert expected_data.issubset(response_data) @pytest.mark.parametrize( @@ -391,7 +410,7 @@ def test_get_observations_for_web_filter_source_by_id_module( for synthese in synthese_data.values() if synthese.id_module in id_modules_selected } - response_data = {feature["properties"]["id"] for feature in r.json["features"]} + response_data = {feature["properties"]["id_synthese"] for feature in r.json["features"]} assert expected_data.issubset(response_data) assert len(response_data) == expected_length @@ -429,7 +448,9 @@ def test_get_synthese_data_cruved(self, app, users, synthese_data, datasets): assert len(features) > 0 for feat in features: - assert feat["properties"]["id"] in [synt.id_synthese for synt in synthese_data.values()] + assert feat["properties"]["id_synthese"] in [ + synt.id_synthese for synt in synthese_data.values() + ] assert response.status_code == 200 def test_get_synthese_data_aggregate(self, users, datasets, synthese_data): @@ -484,35 +505,6 @@ def test_filter_cor_observers(self, users, synthese_data): # le requete doit etre OK marlgré la geom NULL assert response.status_code == 200 - @pytest.mark.parametrize( - "additionnal_column", - [("altitude_min"), ("count_min_max"), ("nom_vern_or_lb_nom")], - ) - def test_get_observations_for_web_param_column_frontend( - self, app, users, synthese_data, additionnal_column - ): - """ - Test de renseigner le paramètre LIST_COLUMNS_FRONTEND pour renvoyer uniquement - les colonnes souhaitées - """ - app.config["SYNTHESE"]["LIST_COLUMNS_FRONTEND"] = [ - { - "prop": additionnal_column, - "name": "My label", - } - ] - - set_logged_user(self.client, users["self_user"]) - - response = self.client.get(url_for("gn_synthese.get_observations_for_web")) - data = response.get_json() - - expected_columns = {"id", "url_source", additionnal_column} - - assert all( - set(feature["properties"].keys()) == expected_columns for feature in data["features"] - ) - @pytest.mark.parametrize( "group_inpn", [ @@ -535,7 +527,7 @@ def test_get_observations_for_web_filter_group_inpn(self, users, synthese_data, ) response_json = response.json assert obs.id_synthese in { - synthese["properties"]["id"] for synthese in response_json["features"] + synthese["properties"]["id_synthese"] for synthese in response_json["features"] } def test_export(self, users): @@ -1403,7 +1395,9 @@ def blur_sensitive_observations(monkeypatch): def get_one_synthese_reponse_from_id(response: dict, id_synthese: int): return [ - synthese for synthese in response["features"] if synthese["properties"]["id"] == id_synthese + synthese + for synthese in response["features"] + if synthese["properties"]["id_synthese"] == id_synthese ][0] @@ -1531,7 +1525,9 @@ def test_get_observations_for_web_blurring_excluded( ] ) - json_synthese_ids = (feature["properties"]["id"] for feature in response_json["features"]) + json_synthese_ids = ( + feature["properties"]["id_synthese"] for feature in response_json["features"] + ) assert all(synthese_id not in json_synthese_ids for synthese_id in sensitive_synthese_ids) def test_get_observations_for_web_blurring_grouped_geom( @@ -1578,7 +1574,7 @@ def test_get_observations_for_web_blurring_grouped_geom( feature["geometry"] is None for feature in json_resp["features"] if all( - observation["id"] in sensitive_synthese_ids + observation["id_synthese"] in sensitive_synthese_ids for observation in feature["properties"]["observations"] ) ) diff --git a/backend/geonature/tests/utils.py b/backend/geonature/tests/utils.py index 1ffb7dda79..8bdc0adf6b 100644 --- a/backend/geonature/tests/utils.py +++ b/backend/geonature/tests/utils.py @@ -29,79 +29,3 @@ def get_id_nomenclature(nomenclature_type_mnemonique, cd_nomenclature): ) ) ) - - -jsonschema_definitions = { - "geometries": { - "BoundingBox": { - "type": "array", - "minItems": 4, - "items": {"type": "number"}, - }, - "PointCoordinates": {"type": "array", "minItems": 2, "items": {"type": "number"}}, - "Point": { - "title": "GeoJSON Point", - "type": "object", - "required": ["type", "coordinates"], - "properties": { - "type": {"type": "string", "enum": ["Point"]}, - "coordinates": { - "$ref": "#/definitions/geometries/PointCoordinates", - }, - "bbox": { - "$ref": "#/definitions/geometries/BoundingBox", - }, - }, - }, - }, - "feature": { - "title": "GeoJSON Feature", - "type": "object", - "required": ["type", "properties", "geometry"], - "properties": { - "type": {"type": "string", "enum": ["Feature"]}, - "id": {"oneOf": [{"type": "number"}, {"type": "string"}]}, - "properties": { - "oneOf": [ - {"type": "null"}, - {"$ref": "#/$defs/props"}, - ], - }, - "geometry": { - "oneOf": [ - {"type": "null"}, - {"$ref": "#/definitions/geometries/Point"}, - # {"$ref": "#/definitions/geometries/LineString"}, - # {"$ref": "#/definitions/geometries/Polygon"}, - # {"$ref": "#/definitions/geometries/MultiPoint"}, - # {"$ref": "#/definitions/geometries/MultiLineString"}, - # {"$ref": "#/definitions/geometries/MultiPolygon"}, - # {"$ref": "#/definitions/geometries/GeometryCollection"}, - ], - }, - "bbox": { - "$ref": "#/definitions/geometries/BoundingBox", - }, - }, - }, - "featurecollection": { - "title": "GeoJSON FeatureCollection", - "type": "object", - "required": ["type", "features"], - "properties": { - "type": { - "type": "string", - "enum": ["FeatureCollection"], - }, - "features": { - "type": "array", - "items": { - "$ref": "#/definitions/feature", - }, - }, - "bbox": { - "$ref": "#/definitions/geometries/BoundingBox", - }, - }, - }, -} diff --git a/frontend/src/app/syntheseModule/synthese-results/synthese-carte/synthese-carte.component.ts b/frontend/src/app/syntheseModule/synthese-results/synthese-carte/synthese-carte.component.ts index a5fcaae3c3..a07fe38849 100644 --- a/frontend/src/app/syntheseModule/synthese-results/synthese-carte/synthese-carte.component.ts +++ b/frontend/src/app/syntheseModule/synthese-results/synthese-carte/synthese-carte.component.ts @@ -278,12 +278,12 @@ export class SyntheseCarteComponent implements OnInit, AfterViewInit, OnChanges, onEachFeature(feature, layer) { // make a cache a layers in a dict with id key - this.layerDictCache(feature.properties.observations.id, layer); + this.layerDictCache(feature.properties.observations.id_synthese, layer); // set style if (this.areasEnable) { - this.setAreasStyle(layer, feature.properties.observations.id.length); + this.setAreasStyle(layer, feature.properties.observations.id_synthese.length); } - this.layerEvent(feature, layer, feature.properties.observations.id); + this.layerEvent(feature, layer, feature.properties.observations.id_synthese); } /** @@ -323,7 +323,7 @@ export class SyntheseCarteComponent implements OnInit, AfterViewInit, OnChanges, const geojsonLayer = new L.GeoJSON(change.inputSyntheseData.currentValue, { pointToLayer: (feature, latlng) => { const circleMarker = L.circleMarker(latlng); - let countObs = feature.properties.observations.id.length; + let countObs = feature.properties.observations.id_synthese.length; (circleMarker as any).nb_obs = countObs; circleMarker.bindTooltip(`${countObs}`, { permanent: true, diff --git a/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-list.component.ts b/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-list.component.ts index 83b8b21e28..9f75aeb7c2 100644 --- a/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-list.component.ts +++ b/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-list.component.ts @@ -67,13 +67,13 @@ export class SyntheseListComponent implements OnInit, OnChanges, AfterContentChe this.mapListService.tableData.map((row) => { // mandatory to sort (each row must have a selected attr) row.selected = false; - if (ids.includes(row.id)) { + if (ids.includes(row.id_synthese)) { row.selected = true; } }); let observations = this.mapListService.tableData.filter((e) => { - return ids.includes(e.id); + return ids.includes(e.id_synthese); }); this.mapListService.tableData.sort((a, b) => { @@ -83,7 +83,7 @@ export class SyntheseListComponent implements OnInit, OnChanges, AfterContentChe this.mapListService.selectedRow = observations; const page = Math.trunc( this.mapListService.tableData.findIndex((e) => { - return e.id === ids[0]; + return e.id_synthese === ids[0]; }) / this.rowNumber ); this.table.offset = page; @@ -111,7 +111,7 @@ export class SyntheseListComponent implements OnInit, OnChanges, AfterContentChe */ onSort(event) { if (event.newValue === undefined) { - let selectedObsIds = this.mapListService.selectedRow.map((obs) => obs.id); + let selectedObsIds = this.mapListService.selectedRow.map((obs) => obs.id_synthese); this.mapListService.mapSelected.next(selectedObsIds); } } @@ -133,7 +133,7 @@ export class SyntheseListComponent implements OnInit, OnChanges, AfterContentChe } openInfoModal(row) { - row.id_synthese = row.id; + row.id_synthese = row.id_synthese; const modalRef = this.ngbModal.open(SyntheseInfoObsComponent, { size: 'lg', windowClass: 'large-modal', diff --git a/frontend/src/app/syntheseModule/synthese.component.ts b/frontend/src/app/syntheseModule/synthese.component.ts index a007e26db9..e1b4108059 100644 --- a/frontend/src/app/syntheseModule/synthese.component.ts +++ b/frontend/src/app/syntheseModule/synthese.component.ts @@ -113,7 +113,7 @@ export class SyntheseComponent implements OnInit { this._mapListService.geojsonData = this.simplifyGeoJson(cloneDeep(data)); this.formatDataForTable(data); - this._mapListService.idName = 'id'; + this._mapListService.idName = 'id_synthese'; this.searchService.dataLoaded = true; }, () => { @@ -137,9 +137,9 @@ export class SyntheseComponent implements OnInit { const idSynthese = new Set(); geojson.features.forEach((feature) => { feature.properties.observations.forEach((obs) => { - if (!idSynthese.has(obs.id)) { + if (!idSynthese.has(obs.id_synthese)) { this._mapListService.tableData.push(obs); - idSynthese.add(obs.id); + idSynthese.add(obs.id_synthese); } }); }); @@ -190,7 +190,7 @@ export class SyntheseComponent implements OnInit { let ids = []; for (let feature of geojson.features) { feature.properties.observations.forEach((obs) => { - ids.push(obs['id']); + ids.push(obs['id_synthese']); }); } return ids; @@ -205,11 +205,11 @@ export class SyntheseComponent implements OnInit { let ids = []; for (let obs of Object.values(feature.properties.observations)) { - if (obs['id']) { - ids.push(obs['id']); + if (obs['id_synthese']) { + ids.push(obs['id_synthese']); } } - feature.properties.observations = { id: ids }; + feature.properties.observations = { id_synthese: ids }; } if (noGeomMessage) { this._toasterService.warning(