diff --git a/backend/dependencies/UsersHub b/backend/dependencies/UsersHub index 29fb1e72ff..ce5d200a23 160000 --- a/backend/dependencies/UsersHub +++ b/backend/dependencies/UsersHub @@ -1 +1 @@ -Subproject commit 29fb1e72ffadfb03e0e3a8bb0e758d47f3bd15f0 +Subproject commit ce5d200a23e19a7826fa4af98418a2ab860ed74b diff --git a/backend/dependencies/UsersHub-authentification-module b/backend/dependencies/UsersHub-authentification-module index e27573a9d1..ac32c2de08 160000 --- a/backend/dependencies/UsersHub-authentification-module +++ b/backend/dependencies/UsersHub-authentification-module @@ -1 +1 @@ -Subproject commit e27573a9d12e116ef91b1211d6cf9451ccd1aae7 +Subproject commit ac32c2de0857d913972cd9356af757379aad322a diff --git a/backend/geonature/core/gn_monitoring/models.py b/backend/geonature/core/gn_monitoring/models.py index b764fa6a00..6ba3a45ca7 100644 --- a/backend/geonature/core/gn_monitoring/models.py +++ b/backend/geonature/core/gn_monitoring/models.py @@ -7,7 +7,7 @@ from geoalchemy2 import Geometry from sqlalchemy import ForeignKey from sqlalchemy.orm import relationship -from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.sql import select, func @@ -16,12 +16,13 @@ from utils_flask_sqla.serializers import serializable from utils_flask_sqla_geo.serializers import geoserializable +from pypnnomenclature.models import TNomenclatures from geonature.core.gn_commons.models import TModules from geonature.core.gn_meta.models import TDatasets from geonature.utils.env import DB -corVisitObserver = DB.Table( +cor_visit_observer = DB.Table( "cor_visit_observer", DB.Column( "id_base_visit", @@ -39,7 +40,7 @@ ) -corSiteModule = DB.Table( +cor_site_module = DB.Table( "cor_site_module", DB.Column( "id_base_site", @@ -56,7 +57,7 @@ schema="gn_monitoring", ) -corSiteArea = DB.Table( +cor_site_area = DB.Table( "cor_site_area", DB.Column( "id_base_site", @@ -68,6 +69,58 @@ schema="gn_monitoring", ) +cor_module_type = DB.Table( + "cor_module_type", + DB.Column( + "id_module", + DB.Integer, + DB.ForeignKey("gn_commons.t_modules.id_module"), + primary_key=True, + ), + DB.Column( + "id_type_site", + DB.Integer, + DB.ForeignKey("gn_monitoring.bib_type_site.id_nomenclature_type_site"), + primary_key=True, + ), + schema="gn_monitoring", +) + +cor_site_type = DB.Table( + "cor_site_type", + DB.Column( + "id_base_site", + DB.Integer, + DB.ForeignKey("gn_monitoring.t_base_sites.id_base_site"), + primary_key=True, + ), + DB.Column( + "id_type_site", + DB.Integer, + DB.ForeignKey("gn_monitoring.bib_type_site.id_nomenclature_type_site"), + primary_key=True, + ), + schema="gn_monitoring", +) + + +@serializable +class BibTypeSite(DB.Model): + __tablename__ = "bib_type_site" + __table_args__ = {"schema": "gn_monitoring"} + + id_nomenclature_type_site = DB.Column( + DB.ForeignKey("ref_nomenclatures.t_nomenclatures.id_nomenclature"), + nullable=False, + primary_key=True, + ) + config = DB.Column(JSONB) + nomenclature = DB.relationship( + TNomenclatures, uselist=False, backref=DB.backref("bib_type_site", uselist=False) + ) + + sites = DB.relationship("TBaseSites", secondary=cor_site_type, lazy="noload") + @serializable class TBaseVisits(DB.Model): @@ -84,7 +137,7 @@ class TBaseVisits(DB.Model): # Pour le moment non défini comme une clé étrangère # pour les questions de perfs # a voir en fonction des usage - id_module = DB.Column(DB.Integer) + id_module = DB.Column(DB.Integer, ForeignKey("gn_commons.t_modules.id_module")) visit_date_min = DB.Column(DB.DateTime) visit_date_max = DB.Column(DB.DateTime) @@ -102,15 +155,16 @@ class TBaseVisits(DB.Model): observers = DB.relationship( User, - secondary=corVisitObserver, - primaryjoin=(corVisitObserver.c.id_base_visit == id_base_visit), - secondaryjoin=(corVisitObserver.c.id_role == User.id_role), - foreign_keys=[corVisitObserver.c.id_base_visit, corVisitObserver.c.id_role], + secondary=cor_visit_observer, + primaryjoin=(cor_visit_observer.c.id_base_visit == id_base_visit), + secondaryjoin=(cor_visit_observer.c.id_role == User.id_role), + foreign_keys=[cor_visit_observer.c.id_base_visit, cor_visit_observer.c.id_role], ) + observers_txt = DB.Column(DB.Unicode) + dataset = relationship( TDatasets, - lazy="joined", primaryjoin=(TDatasets.id_dataset == id_dataset), foreign_keys=[id_dataset], ) @@ -128,7 +182,6 @@ class TBaseSites(DB.Model): id_base_site = DB.Column(DB.Integer, primary_key=True) id_inventor = DB.Column(DB.Integer, ForeignKey("utilisateurs.t_roles.id_role")) id_digitiser = DB.Column(DB.Integer, ForeignKey("utilisateurs.t_roles.id_role")) - id_nomenclature_type_site = DB.Column(DB.Integer) base_site_name = DB.Column(DB.Unicode) base_site_description = DB.Column(DB.Unicode) base_site_code = DB.Column(DB.Unicode) @@ -153,8 +206,23 @@ class TBaseSites(DB.Model): "TModules", lazy="select", enable_typechecks=False, - secondary=corSiteModule, - primaryjoin=(corSiteModule.c.id_base_site == id_base_site), - secondaryjoin=(corSiteModule.c.id_module == TModules.id_module), - foreign_keys=[corSiteModule.c.id_base_site, corSiteModule.c.id_module], + secondary=cor_site_module, + primaryjoin=(cor_site_module.c.id_base_site == id_base_site), + secondaryjoin=(cor_site_module.c.id_module == TModules.id_module), + foreign_keys=[cor_site_module.c.id_base_site, cor_site_module.c.id_module], + ) + + +@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")) + id_digitiser = DB.Column(DB.Integer, DB.ForeignKey("utilisateurs.t_roles.id_role")) + digitiser = DB.relationship( + User, primaryjoin=(User.id_role == id_digitiser), foreign_keys=[id_digitiser] ) + cd_nom = DB.Column(DB.Integer) + comments = DB.Column(DB.String) + uuid_observation = DB.Column(UUID(as_uuid=True), default=select(func.uuid_generate_v4())) diff --git a/backend/geonature/core/gn_monitoring/routes.py b/backend/geonature/core/gn_monitoring/routes.py index 0ad5889009..245ed06047 100644 --- a/backend/geonature/core/gn_monitoring/routes.py +++ b/backend/geonature/core/gn_monitoring/routes.py @@ -1,6 +1,6 @@ from flask import Blueprint, request from geojson import FeatureCollection -from geonature.core.gn_monitoring.models import TBaseSites, corSiteArea, corSiteModule +from geonature.core.gn_monitoring.models import TBaseSites, cor_site_area, cor_site_module from geonature.utils.env import DB from ref_geo.models import LAreas from sqlalchemy import select @@ -79,17 +79,16 @@ def get_site_areas(id_site): params = request.args query = ( - # TODO@LAreas.geom_4326 - select(corSiteArea, func.ST_Transform(LAreas.geom, 4326)) - .join(LAreas, LAreas.id_area == corSiteArea.c.id_area) - .where(corSiteArea.c.id_base_site == id_site) + select(cor_site_area, func.ST_Transform(LAreas.geom, 4326)) + .join(LAreas, LAreas.id_area == cor_site_area.c.id_area) + .where(cor_site_area.c.id_base_site == id_site) ) if "id_area_type" in params: query = query.where(LAreas.id_type == params["id_area_type"]) if "id_module" in params: - query = query.join(corSiteModule, corSiteModule.c.id_base_site == id_site).where( - corSiteModule.c.id_module == params["id_module"] + query = query.join(cor_site_module, cor_site_module.c.id_base_site == id_site).where( + cor_site_module.c.id_module == params["id_module"] ) data = DB.session.scalars(query).all() diff --git a/backend/geonature/core/gn_synthese/routes.py b/backend/geonature/core/gn_synthese/routes.py index c5717a1498..2a803d79ef 100644 --- a/backend/geonature/core/gn_synthese/routes.py +++ b/backend/geonature/core/gn_synthese/routes.py @@ -958,10 +958,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_ref): +def taxon_stats(scope, cd_nom): """Return stats for a specific taxon""" area_type = request.args.get("area_type") @@ -986,7 +986,7 @@ def taxon_stats(scope, cd_ref): .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 @@ -1021,7 +1021,7 @@ def taxon_stats(scope, cd_ref): synthese_stats = result.fetchone() data = { - "cd_ref": cd_ref, + "cd_ref": cd_nom, "observation_count": synthese_stats["observation_count"], "observer_count": synthese_stats["observer_count"], "area_count": synthese_stats["area_count"], diff --git a/backend/geonature/core/imports/checks/sql/core.py b/backend/geonature/core/imports/checks/sql/core.py index ad2d6a52dc..c2c62200c3 100644 --- a/backend/geonature/core/imports/checks/sql/core.py +++ b/backend/geonature/core/imports/checks/sql/core.py @@ -15,7 +15,7 @@ __all__ = ["init_rows_validity", "check_orphan_rows"] -def init_rows_validity(imprt: TImports): +def init_rows_validity(imprt: TImports, dataset_name_field: str = "id_dataset"): """ Validity columns are three-states: - None: the row does not contains data for the given entity @@ -48,16 +48,21 @@ def init_rows_validity(imprt: TImports): .where(BibFields.name_field.in_(selected_fields_names)) .where(BibFields.entities.any(EntityField.entity == entity)) .where(~BibFields.entities.any(EntityField.entity != entity)) + .where(BibFields.name_field != dataset_name_field) .all() ) - db.session.execute( - sa.update(transient_table) - .where(transient_table.c.id_import == imprt.id_import) - .where( - sa.or_(*[transient_table.c[field.source_column].isnot(None) for field in fields]) + + if fields: + db.session.execute( + sa.update(transient_table) + .where(transient_table.c.id_import == imprt.id_import) + .where( + sa.or_( + *[transient_table.c[field.source_column].isnot(None) for field in fields] + ) + ) + .values({entity.validity_column: True}) ) - .values({entity.validity_column: True}) - ) def check_orphan_rows(imprt: TImports): diff --git a/backend/geonature/core/imports/models.py b/backend/geonature/core/imports/models.py index b070493a46..c516143ffd 100644 --- a/backend/geonature/core/imports/models.py +++ b/backend/geonature/core/imports/models.py @@ -1,7 +1,7 @@ from datetime import datetime from collections.abc import Mapping import re -from typing import Iterable, List, Optional +from typing import Any, Iterable, List, Optional from packaging import version from flask import g @@ -618,6 +618,42 @@ def optional_conditions_to_jsonschema(name_field: str, optional_conditions: Iter } +# TODO move to utils lib +def get_fields_of_an_entity( + entity: "Entity", + columns: Optional[List[str]] = None, + optional_where_clause: Optional[Any] = None, +) -> List["BibFields"]: + """ + Get all BibFields associated with a given entity. + + Parameters + ---------- + entity : Entity + The entity to get the fields for. + columns : Optional[List[str]], optional + The columns to retrieve. If None, all columns are retrieved. + optional_where_clause : Optional[Any], optional + An optional where clause to apply to the query. + + Returns + ------- + List[BibFields] + The BibFields associated with the given entity. + """ + select_args = [BibFields] + query = sa.select(BibFields).where( + BibFields.entities.any(EntityField.entity == entity), + ) + if columns: + select_args = [getattr(BibFields, col) for col in columns] + query.with_only_columns(*select_args) + if optional_where_clause is not None: + query = query.where(optional_where_clause) + + return db.session.scalars(query).all() + + @serializable class FieldMapping(MappingTemplate): __tablename__ = "t_fieldmappings" @@ -631,19 +667,60 @@ class FieldMapping(MappingTemplate): } @staticmethod - def validate_values(values): - fields = ( - BibFields.query.filter_by(destination=g.destination, display=True) - .with_entities( - BibFields.name_field, - BibFields.autogenerated, - BibFields.mandatory, - BibFields.multi, - BibFields.optional_conditions, - BibFields.mandatory_conditions, - ) - .all() + def validate_values(field_mapping_json): + """ + Validate the field mapping values returned by the client form. + + Parameters + ---------- + field_mapping_json : dict + The field mapping values. + + Raises + ------ + ValueError + If the field mapping values are invalid. + """ + bib_fields_col = [ + "name_field", + "autogenerated", + "mandatory", + "multi", + "optional_conditions", + "mandatory_conditions", + ] + entities_for_destination: List[Entity] = ( + Entity.query.filter_by(destination=g.destination).order_by(sa.desc(Entity.order)).all() ) + fields = [] + for entity in entities_for_destination: + # Get fields associated to this entity and exists in the given field mapping + fields_of_ent = get_fields_of_an_entity( + entity, + columns=bib_fields_col, + optional_where_clause=sa.and_( + sa.or_( + ~BibFields.entities.any(EntityField.entity != entity), + BibFields.name_field == entity.unique_column.name_field, + ), + BibFields.name_field.in_(field_mapping_json.keys()), + ), + ) + + # if the only column corresponds to id_columns, we only do the validation on the latter + if [entity.unique_column.name_field] == [f.name_field for f in fields_of_ent]: + fields.extend(fields_of_ent) + else: + # if other columns than the id_columns are used, we need to check every fields of this entity + fields.extend( + get_fields_of_an_entity( + entity, + columns=bib_fields_col, + optional_where_clause=sa.and_( + BibFields.destination == g.destination, BibFields.display == True + ), + ) + ) schema = { "type": "object", @@ -694,7 +771,7 @@ def validate_values(values): schema["allOf"] = optional_conditions try: - validate_json(values, schema) + validate_json(field_mapping_json, schema) except JSONValidationError as e: raise ValueError(e.message) diff --git a/backend/geonature/core/imports/utils.py b/backend/geonature/core/imports/utils.py index 81c3089e32..e50703a7e5 100644 --- a/backend/geonature/core/imports/utils.py +++ b/backend/geonature/core/imports/utils.py @@ -406,6 +406,8 @@ def update_transient_data_from_dataframe( updated_cols = ["id_import", "line_no"] + list(updated_cols) dataframe.replace({np.nan: None}, inplace=True) records = dataframe[updated_cols].to_dict(orient="records") + if not records: + return insert_stmt = pg_insert(transient_table) insert_stmt = insert_stmt.values(records).on_conflict_do_update( index_elements=updated_cols[:2], diff --git a/backend/geonature/migrations/versions/6734d8f7eb2a_monitoring_add_id_digitizer_to_t_observations.py b/backend/geonature/migrations/versions/6734d8f7eb2a_monitoring_add_id_digitizer_to_t_observations.py new file mode 100644 index 0000000000..f17add3506 --- /dev/null +++ b/backend/geonature/migrations/versions/6734d8f7eb2a_monitoring_add_id_digitizer_to_t_observations.py @@ -0,0 +1,67 @@ +"""[monitoring] add id_digitizer to t_observations + +Revision ID: 6734d8f7eb2a +Revises: 9b88459c1298 +Create Date: 2024-01-16 15:50:30.308266 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "6734d8f7eb2a" +down_revision = "9b88459c1298" +branch_labels = None +depends_on = None + + +monitorings_schema = "gn_monitoring" +table = "t_observations" +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, + ) + op.execute( + """ + UPDATE gn_monitoring.t_observations o SET id_digitiser = tbv.id_digitiser + FROM gn_monitoring.t_base_visits AS tbv + WHERE tbv.id_base_visit = o.id_base_visit; + """ + ) + # Set not null constraint + op.alter_column( + table_name=table, + column_name=column, + existing_type=sa.Integer(), + nullable=False, + 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/geonature/migrations/versions/8309591841f3_monitoring_add_observers_txt_column_t_base_visit.py b/backend/geonature/migrations/versions/8309591841f3_monitoring_add_observers_txt_column_t_base_visit.py new file mode 100644 index 0000000000..9ffe7de1de --- /dev/null +++ b/backend/geonature/migrations/versions/8309591841f3_monitoring_add_observers_txt_column_t_base_visit.py @@ -0,0 +1,38 @@ +"""[monitoring] add_observers_txt_column_t_base_visit + +Revision ID: 8309591841f3 +Revises: 7b6a578eccd7 +Create Date: 2023-10-06 11:07:43.532623 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "8309591841f3" +down_revision = "7b6a578eccd7" +branch_labels = None +depends_on = None + + +monitorings_schema = "gn_monitoring" +table = "t_base_visits" +column = "observers_txt" + + +def upgrade(): + op.add_column( + table, + sa.Column( + column, + sa.Text(), + nullable=True, + ), + schema=monitorings_schema, + ) + + +def downgrade(): + op.drop_column(table, column, schema=monitorings_schema) diff --git a/backend/geonature/migrations/versions/8b149244d586_add_required_conditions_in_import.py b/backend/geonature/migrations/versions/8b149244d586_add_required_conditions_in_import.py index 5ddd7b7c36..ba08741922 100644 --- a/backend/geonature/migrations/versions/8b149244d586_add_required_conditions_in_import.py +++ b/backend/geonature/migrations/versions/8b149244d586_add_required_conditions_in_import.py @@ -66,14 +66,16 @@ def upgrade(): op.execute( """ - alter table gn_imports.bib_fields - ADD CONSTRAINT mandatory_conditions_field_exists CHECK (gn_imports.isInNameFields(mandatory_conditions,id_destination)); + ALTER TABLE gn_imports.bib_fields + ADD CONSTRAINT mandatory_conditions_field_exists CHECK (gn_imports.isInNameFields(mandatory_conditions,id_destination)) + NOT VALID; """ ) op.execute( """ - alter table gn_imports.bib_fields - ADD CONSTRAINT optional_conditions_field_exists CHECK (gn_imports.isInNameFields(optional_conditions,id_destination)); + ALTER TABLE gn_imports.bib_fields + ADD CONSTRAINT optional_conditions_field_exists CHECK (gn_imports.isInNameFields(optional_conditions,id_destination)) + NOT VALID; """ ) conn = op.get_bind() diff --git a/backend/geonature/migrations/versions/9b88459c1298_monitoring_create_t_observations.py b/backend/geonature/migrations/versions/9b88459c1298_monitoring_create_t_observations.py new file mode 100644 index 0000000000..7e5b55d11b --- /dev/null +++ b/backend/geonature/migrations/versions/9b88459c1298_monitoring_create_t_observations.py @@ -0,0 +1,55 @@ +"""[monitoring] create t_observations + +Revision ID: 9b88459c1298 +Revises: a54bafb13ce8 +Create Date: 2024-01-16 15:41:13.331912 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "9b88459c1298" +down_revision = "a54bafb13ce8" +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute( + """ + 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 + ); + """ + ) + op.execute( + """ + INSERT INTO gn_commons.bib_tables_location(table_desc, schema_name, table_name, pk_field, uuid_field_name) + VALUES + ('Table centralisant les observations réalisées lors d''une visite sur un site', 'gn_monitoring', 't_observations', 'id_observation', 'uuid_observation') + ON CONFLICT(schema_name, table_name) DO NOTHING; + """ + ) + + +def downgrade(): + op.execute( + """ + DELETE FROM gn_commons.bib_tables_location + WHERE schema_name = 'gn_monitoring' AND table_name = 't_observations'; + """ + ) + op.drop_table("t_observations", schema="gn_monitoring") diff --git a/backend/geonature/migrations/versions/a54bafb13ce8_monitoring_create_cor_module_type.py b/backend/geonature/migrations/versions/a54bafb13ce8_monitoring_create_cor_module_type.py new file mode 100644 index 0000000000..3c867d9526 --- /dev/null +++ b/backend/geonature/migrations/versions/a54bafb13ce8_monitoring_create_cor_module_type.py @@ -0,0 +1,64 @@ +""" [monitoring] create_cor_module_type + +Revision ID: a54bafb13ce8 +Revises: ce54ba49ce5c +Create Date: 2022-12-06 16:18:24.512562 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "a54bafb13ce8" +down_revision = "ce54ba49ce5c" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" +referent_schema = "gn_commons" + + +def upgrade(): + op.create_table( + "cor_module_type", + sa.Column( + "id_type_site", + sa.Integer(), + sa.ForeignKey( + f"{monitorings_schema}.bib_type_site.id_nomenclature_type_site", + name="fk_cor_module_type_id_nomenclature", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=False, + ), + sa.Column( + "id_module", + sa.Integer(), + sa.ForeignKey( + f"{referent_schema}.t_modules.id_module", + name="fk_cor_module_type_id_module", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=False, + ), + sa.PrimaryKeyConstraint("id_type_site", "id_module", name="pk_cor_module_type"), + schema=monitorings_schema, + ) + + # Insertion des données a partir de cor_site_module + op.execute( + """ + INSERT INTO gn_monitoring.cor_module_type (id_module, id_type_site ) + SELECT DISTINCT csm.id_module, cts.id_type_site + FROM gn_monitoring.cor_site_module AS csm + JOIN gn_monitoring.cor_site_type AS cts + ON Cts.id_base_site = csm.id_base_site ; + """ + ) + + +def downgrade(): + op.drop_table("cor_module_type", schema=monitorings_schema) diff --git a/backend/geonature/migrations/versions/b53bafb13ce8_monitoring_create_bib_type_site.py b/backend/geonature/migrations/versions/b53bafb13ce8_monitoring_create_bib_type_site.py new file mode 100644 index 0000000000..0c66d8ece2 --- /dev/null +++ b/backend/geonature/migrations/versions/b53bafb13ce8_monitoring_create_bib_type_site.py @@ -0,0 +1,77 @@ +"""[monitoring] create_bib_type_site + +Revision ID: b53bafb13ce8 +Revises: 8309591841f3 +Create Date: 2022-12-06 16:18:24.512562 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "b53bafb13ce8" +down_revision = "8309591841f3" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" +nomenclature_schema = "ref_nomenclatures" + + +def upgrade(): + op.create_table( + "bib_type_site", + sa.Column( + "id_nomenclature_type_site", + sa.Integer(), + sa.ForeignKey( + f"{nomenclature_schema}.t_nomenclatures.id_nomenclature", + name="fk_t_nomenclatures_id_nomenclature_type_site", + ), + nullable=False, + unique=True, + ), + sa.PrimaryKeyConstraint("id_nomenclature_type_site"), + sa.Column("config", sa.JSON(), nullable=True), + schema=monitorings_schema, + ) + + # FIXME: if sqlalchemy >= 1.4.32, it should work with postgresql_not_valid=True: cleaner + # op.create_check_constraint( + # "ck_bib_type_site_id_nomenclature_type_site", + # "bib_type_site", + # f"{nomenclature_schema}.check_nomenclature_type_by_mnemonique(id_nomenclature_type_site,'TYPE_SITE')", + # schema=monitorings_schema, + # postgresql_not_valid=True + # ) + statement = sa.text( + f""" + ALTER TABLE {monitorings_schema}.bib_type_site + ADD + CONSTRAINT ck_bib_type_site_id_nomenclature_type_site CHECK ( + {nomenclature_schema}.check_nomenclature_type_by_mnemonique( + id_nomenclature_type_site, 'TYPE_SITE' :: character varying + ) + ) NOT VALID + """ + ) + op.execute(statement) + op.create_table_comment( + "bib_type_site", + "Table de définition des champs associés aux types de sites", + schema=monitorings_schema, + ) + + # Récupération de la liste des types de site avec ceux déja présents dans la table t_base_site + op.execute( + """ + INSERT INTO gn_monitoring.bib_type_site AS bts (id_nomenclature_type_site) + SELECT DISTINCT id_nomenclature_type_site + FROM gn_monitoring.t_base_sites AS tbs ; + """ + ) + + +def downgrade(): + op.drop_table("bib_type_site", schema=monitorings_schema) diff --git a/backend/geonature/migrations/versions/ce54ba49ce5c_monitoring_create_cor_site_type.py b/backend/geonature/migrations/versions/ce54ba49ce5c_monitoring_create_cor_site_type.py new file mode 100644 index 0000000000..d1a4296713 --- /dev/null +++ b/backend/geonature/migrations/versions/ce54ba49ce5c_monitoring_create_cor_site_type.py @@ -0,0 +1,130 @@ +"""[monitoring] create_cor_site_type + +Revision ID: ce54ba49ce5c +Revises: b53bafb13ce8 +Create Date: 2022-12-06 16:18:24.512562 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "ce54ba49ce5c" +down_revision = "b53bafb13ce8" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" + + +def upgrade(): + op.create_table( + "cor_site_type", + sa.Column( + "id_type_site", + sa.Integer(), + sa.ForeignKey( + f"{monitorings_schema}.bib_type_site.id_nomenclature_type_site", + name="fk_cor_site_type_id_nomenclature_type_site", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=False, + ), + sa.Column( + "id_base_site", + sa.Integer(), + sa.ForeignKey( + f"{monitorings_schema}.t_base_sites.id_base_site", + name="fk_cor_site_type_id_base_site", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=False, + ), + sa.PrimaryKeyConstraint("id_type_site", "id_base_site", name="pk_cor_site_type"), + schema=monitorings_schema, + ) + op.create_table_comment( + "cor_site_type", + "Table d'association entre les sites et les types de sites", + schema=monitorings_schema, + ) + + op.execute( + """ + INSERT INTO gn_monitoring.cor_site_type + SELECT id_nomenclature_type_site , id_base_site d + FROM gn_monitoring.t_base_sites ; + """ + ) + + op.execute( + """ + ALTER TABLE gn_monitoring.t_base_sites + DROP CONSTRAINT check_t_base_sites_type_site; + """ + ) + op.execute( + """ + DROP INDEX gn_monitoring.idx_t_base_sites_type_site; + """ + ) + op.drop_column( + table_name="t_base_sites", + column_name="id_nomenclature_type_site", + schema=monitorings_schema, + ) + + +def downgrade(): + op.add_column( + table_name="t_base_sites", + column=sa.Column( + "id_nomenclature_type_site", + sa.Integer(), + sa.ForeignKey( + "ref_nomenclatures.t_nomenclatures.id_nomenclature", + name="fk_t_base_sites_type_site", + onupdate="CASCADE", + ), + ), + schema=monitorings_schema, + ) + + op.execute( + """ + WITH ts AS ( + SELECT DISTINCT ON (id_base_site) id_base_site, id_type_site + FROM gn_monitoring.cor_site_type AS cts + ORDER BY id_base_site, id_type_site + ) + UPDATE gn_monitoring.t_base_sites tbs + SET id_nomenclature_type_site = id_type_site + FROM ts + WHERE ts.id_base_site = tbs.id_base_site; + """ + ) + + op.execute( + """ + ALTER TABLE gn_monitoring.t_base_sites + ADD CONSTRAINT check_t_base_sites_type_site + CHECK (ref_nomenclatures.check_nomenclature_type_by_mnemonique(id_nomenclature_type_site,'TYPE_SITE')) + NOT VALID; + """ + ) + + op.execute( + """ + CREATE INDEX idx_t_base_sites_type_site ON gn_monitoring.t_base_sites USING btree (id_nomenclature_type_site); + """ + ) + op.execute( + """ + ALTER TABLE gn_monitoring.t_base_sites ALTER COLUMN id_nomenclature_type_site SET NOT NULL; + """ + ) + + op.drop_table("cor_site_type", schema=monitorings_schema) diff --git a/backend/geonature/migrations/versions/ebbe0f7ed866_declare_taxhub_admin.py b/backend/geonature/migrations/versions/ebbe0f7ed866_declare_taxhub_admin.py index 55390d66c4..61d1f4d71c 100644 --- a/backend/geonature/migrations/versions/ebbe0f7ed866_declare_taxhub_admin.py +++ b/backend/geonature/migrations/versions/ebbe0f7ed866_declare_taxhub_admin.py @@ -24,8 +24,8 @@ def upgrade(): sa.text( """ INSERT INTO gn_commons.t_modules - (module_code, module_label, module_picto, module_desc, module_target, module_external_url, active_frontend, active_backend) - VALUES('TAXHUB', 'TaxHub', 'fa-sitemap', 'Module TaxHub', '_blank', :module_url, false, false); + (module_code, module_label, module_picto, module_desc, module_target, module_external_url ,active_frontend, active_backend) + VALUES('TAXHUB', 'TaxHub', 'fa-sitemap', 'Module TaxHub', '_blank','', false, false); INSERT INTO gn_permissions.t_objects (code_object, description_object) @@ -89,8 +89,7 @@ def upgrade(): gn_permissions.bib_actions a ON a.code_action = v.action_code WHERE m.module_code = 'TAXHUB' """ - ), - module_url=f"{config['API_ENDPOINT']}/admin/taxons", + ) ) # rapatriement des permissions de l'application TaxHub diff --git a/backend/geonature/migrations/versions/imports/2b0b3bd0248c_multidest.py b/backend/geonature/migrations/versions/imports/2b0b3bd0248c_multidest.py index fdf7faf601..a7cae4f2d0 100644 --- a/backend/geonature/migrations/versions/imports/2b0b3bd0248c_multidest.py +++ b/backend/geonature/migrations/versions/imports/2b0b3bd0248c_multidest.py @@ -376,7 +376,8 @@ def upgrade(): f""" UPDATE gn_commons.cor_module_dataset SET id_module = {ID_MODULE_SYNTHESE} - WHERE id_module = {ID_MODULE_IMPORT}; + WHERE id_module = {ID_MODULE_IMPORT} + AND NOT EXISTS(select 1 from gn_commons.cor_module_dataset b where b.id_dataset = id_dataset and b.id_module={ID_MODULE_SYNTHESE} ); """ ) diff --git a/backend/geonature/tests/test_synthese.py b/backend/geonature/tests/test_synthese.py index 84f604d07b..58384feb4f 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_ref=CD_REF_VALID), + url_for("gn_synthese.taxon_stats", cd_nom=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_ref=CD_REF_VALID, area_type=AREA_TYPE_INVALID), + 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" # Invalid cd_ref parameter response = self.client.get( - url_for("gn_synthese.taxon_stats", cd_ref=CD_REF_INVALID, area_type=AREA_TYPE_VALID), + url_for("gn_synthese.taxon_stats", cd_nom=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_ref=CD_REF_VALID, area_type=AREA_TYPE_VALID), + url_for("gn_synthese.taxon_stats", cd_nom=CD_REF_VALID, area_type=AREA_TYPE_VALID), ) response_json = response.get_json() assert response.status_code == 200 diff --git a/backend/requirements-common.in b/backend/requirements-common.in index 8176b7673b..c031ee223f 100644 --- a/backend/requirements-common.in +++ b/backend/requirements-common.in @@ -32,6 +32,7 @@ shapely>=2.0.0 sqlalchemy<2.0 toml weasyprint -wtforms +# Compatibility with flask admin +wtforms==3.1.2 wtforms-sqlalchemy xmltodict diff --git a/backend/requirements-dependencies.in b/backend/requirements-dependencies.in index 56c86a235c..3c03b9878a 100644 --- a/backend/requirements-dependencies.in +++ b/backend/requirements-dependencies.in @@ -1,4 +1,4 @@ -pypnusershub>=3.0.0,<4 +pypnusershub>=3.0.1,<4 pypnnomenclature>=1.6.4,<2 pypn_habref_api>=0.4.1,<1 utils-flask-sqlalchemy-geo>=0.3.2,<1 diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index d57d4455b4..7ec3665bd3 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -54,7 +54,7 @@ authlib==1.3.2 # via pypnusershub babel==2.16.0 # via flask-babel -bcrypt==4.2.0 +bcrypt==4.2.1 # via pypnusershub billiard==4.2.1 # via celery @@ -108,15 +108,11 @@ cryptography==43.0.3 # via authlib cssselect2==0.7.0 # via weasyprint -dnspython==2.7.0 - # via email-validator -email-validator==2.2.0 - # via wtforms-components fiona==1.10.1 # via # -r requirements-common.in # utils-flask-sqlalchemy-geo -flask==3.0.3 +flask==3.1.0 # via # -r requirements-common.in # flask-admin @@ -183,8 +179,10 @@ flask-wtf==1.2.2 # via # -r requirements-common.in # usershub -fonttools[woff]==4.54.1 - # via weasyprint +fonttools[woff]==4.55.0 + # via + # fonttools + # weasyprint geoalchemy2==0.16.0 # via utils-flask-sqlalchemy-geo geojson==3.1.0 @@ -199,9 +197,7 @@ gunicorn==23.0.0 # taxhub # usershub idna==3.10 - # via - # email-validator - # requests + # via requests importlib-metadata==4.13.0 ; python_version < "3.10" # via # -r requirements-common.in @@ -337,9 +333,7 @@ shapely==2.0.6 # -r requirements-common.in # utils-flask-sqlalchemy-geo six==1.16.0 - # via - # python-dateutil - # wtforms-components + # via python-dateutil sqlalchemy==1.4.54 # via # -r requirements-common.in @@ -402,7 +396,7 @@ werkzeug==3.1.3 # via # flask # flask-login -wtforms==3.2.1 +wtforms==3.1.2 # via # -r requirements-common.in # flask-admin @@ -410,7 +404,7 @@ wtforms==3.2.1 # usershub # wtforms-components # wtforms-sqlalchemy -wtforms-components==0.10.5 +wtforms-components==0.11.0 # via usershub wtforms-sqlalchemy==0.4.2 # via -r requirements-common.in diff --git a/backend/requirements.txt b/backend/requirements.txt index bc821e853f..8cc794174a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -9,7 +9,7 @@ alembic==1.14.0 # flask-migrate # pypn-ref-geo # pypnusershub -amqp==5.2.0 +amqp==5.3.1 # via kombu async-timeout==5.0.1 # via redis @@ -22,7 +22,7 @@ authlib==1.3.2 # via pypnusershub babel==2.16.0 # via flask-babel -bcrypt==4.2.0 +bcrypt==4.2.1 # via pypnusershub billiard==4.2.1 # via celery @@ -80,7 +80,7 @@ fiona==1.10.1 # via # -r requirements-common.in # utils-flask-sqlalchemy-geo -flask==3.0.3 +flask==3.1.0 # via # -r requirements-common.in # flask-admin @@ -142,9 +142,11 @@ flask-weasyprint==1.1.0 # via -r requirements-common.in flask-wtf==1.2.2 # via -r requirements-common.in -fonttools[woff]==4.54.1 - # via weasyprint -geoalchemy2==0.15.2 +fonttools[woff]==4.55.0 + # via + # fonttools + # weasyprint +geoalchemy2==0.16.0 # via utils-flask-sqlalchemy-geo geojson==3.1.0 # via @@ -253,7 +255,7 @@ pypn-ref-geo==1.5.4 # taxhub pypnnomenclature==1.6.4 # via -r requirements-dependencies.in -pypnusershub==3.0.0 +pypnusershub==3.0.1 # via # -r requirements-dependencies.in # taxhub @@ -374,7 +376,7 @@ werkzeug==3.1.3 # via # flask # flask-login -wtforms==3.2.1 +wtforms==3.1.2 # via # -r requirements-common.in # flask-admin diff --git a/config/default_config.toml.example b/config/default_config.toml.example index 012c784109..c6a2e164c6 100644 --- a/config/default_config.toml.example +++ b/config/default_config.toml.example @@ -145,7 +145,7 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" #Information de l'observation à mettre dans l'email DISPLAY_EMAIL_DISPLAY_INFO = ["NOM_VERN", "DATE", "COMMUNES", "MEDIAS"] - # Activer l'affichage des informations liées aux profils de taxons + # Activer l'affichage des informations liées aux profils de taxons (dans les modules Validation, Synthèse et Occtax) ENABLE_PROFILES = true # Configuration de l'affichage des cartes dans GeoNature @@ -166,7 +166,7 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" ZOOM_ON_CLICK = 16 # activation d'un boutton de géolocalisation sur la carte - GEOLOCATION = true + GEOLOCATION = false # Restreindre la recherche OpenStreetMap (sur la carte dans l'encart "Rechercher un lieu") # à certains pays. Les pays doivent être au format ISO_3166-1 : @@ -443,9 +443,9 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" [SYNTHESE.TAXON_SHEET] # Options dédiées à la fiche taxon - # Permet d'activer ou non la section "Profile" + # Permet d'activer ou non l'onglet "Profil" ENABLE_PROFILE = true - # Permet d'activer ou non la section "Taxonomy" + # Permet d'activer ou non l'onglet "Taxonomie" ENABLE_TAXONOMY = true # Gestion des demandes d'inscription diff --git a/contrib/gn_module_occhab/backend/gn_module_occhab/imports/actions.py b/contrib/gn_module_occhab/backend/gn_module_occhab/imports/actions.py index 0ea596df80..1ecddc529c 100644 --- a/contrib/gn_module_occhab/backend/gn_module_occhab/imports/actions.py +++ b/contrib/gn_module_occhab/backend/gn_module_occhab/imports/actions.py @@ -91,13 +91,14 @@ def preprocess_transient_data(imprt: TImports, df) -> set: .where(BibFields.destination == imprt.destination) .where(BibFields.name_field == "date_max") ).scalar_one() - updated_cols |= concat_dates( - df, - datetime_min_col=date_min_field.source_field, - datetime_max_col=date_max_field.source_field, - date_min_col=date_min_field.source_field, - date_max_col=date_max_field.source_field, - ) + if date_min_field.source_field in df and date_max_field.source_field in df: + updated_cols |= concat_dates( + df, + datetime_min_col=date_min_field.source_field, + datetime_max_col=date_max_field.source_field, + date_min_col=date_min_field.source_field, + date_max_col=date_max_field.source_field, + ) return updated_cols @staticmethod diff --git a/contrib/gn_module_occhab/backend/gn_module_occhab/migrations/167d69b42d25_import.py b/contrib/gn_module_occhab/backend/gn_module_occhab/migrations/167d69b42d25_import.py index 3156b11faa..879e626273 100644 --- a/contrib/gn_module_occhab/backend/gn_module_occhab/migrations/167d69b42d25_import.py +++ b/contrib/gn_module_occhab/backend/gn_module_occhab/migrations/167d69b42d25_import.py @@ -18,7 +18,7 @@ revision = "167d69b42d25" down_revision = "f305718b81d3" branch_labels = None -depends_on = ("92f0083cf735",) +depends_on = ("7b6a578eccd7",) def upgrade(): diff --git a/contrib/gn_module_occhab/frontend/app/components/occhab-map-list/occhab-map-list.component.ts b/contrib/gn_module_occhab/frontend/app/components/occhab-map-list/occhab-map-list.component.ts index aadcb210a9..7f4dc31c46 100644 --- a/contrib/gn_module_occhab/frontend/app/components/occhab-map-list/occhab-map-list.component.ts +++ b/contrib/gn_module_occhab/frontend/app/components/occhab-map-list/occhab-map-list.component.ts @@ -47,8 +47,11 @@ export class OccHabMapListComponent implements OnInit { // get user cruved const currentModule = this._moduleService.currentModule; this.userCruved = currentModule.cruved; - const cruvedImport = - this.cruvedStore.cruved.IMPORT.module_objects.IMPORT.cruved; + let cruvedImport: any = {}; + if (this.cruvedStore.cruved.IMPORT) { + cruvedImport = + this.cruvedStore.cruved.IMPORT.module_objects.IMPORT.cruved; + } const canCreateImport = cruvedImport.C > 0; const canCreateOcchab = this.userCruved.C > 0; diff --git a/contrib/gn_module_occhab/frontend/app/services/form-service.ts b/contrib/gn_module_occhab/frontend/app/services/form-service.ts index e933a04170..000c19a127 100644 --- a/contrib/gn_module_occhab/frontend/app/services/form-service.ts +++ b/contrib/gn_module_occhab/frontend/app/services/form-service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable } from "@angular/core"; import { UntypedFormBuilder, UntypedFormGroup, @@ -6,13 +6,13 @@ import { Validators, AbstractControl, UntypedFormArray, -} from '@angular/forms'; -import { NgbDateParserFormatter } from '@ng-bootstrap/ng-bootstrap'; -import { FormService } from '@geonature_common/form/form.service'; -import { DataFormService } from '@geonature_common/form/data-form.service'; -import { OcchabStoreService } from './store.service'; -import { ConfigService } from '@geonature/services/config.service'; -import { Station, StationFeature } from '../models'; +} from "@angular/forms"; +import { NgbDateParserFormatter } from "@ng-bootstrap/ng-bootstrap"; +import { FormService } from "@geonature_common/form/form.service"; +import { DataFormService } from "@geonature_common/form/data-form.service"; +import { OcchabStoreService } from "./store.service"; +import { ConfigService } from "@geonature/services/config.service"; +import { Station, StationFeature } from "../models"; @Injectable() export class OcchabFormService { @@ -42,8 +42,14 @@ export class OcchabFormService { id_dataset: [null, Validators.required], date_min: [null, Validators.required], date_max: [null, Validators.required], - observers: [null, !this.config.OCCHAB.OBSERVER_AS_TXT ? Validators.required : null], - observers_txt: [null, this.config.OCCHAB.OBSERVER_AS_TXT ? Validators.required : null], + observers: [ + null, + !this.config.OCCHAB.OBSERVER_AS_TXT ? Validators.required : null, + ], + observers_txt: [ + null, + this.config.OCCHAB.OBSERVER_AS_TXT ? Validators.required : null, + ], is_habitat_complex: false, id_nomenclature_exposure: null, altitude_min: null, @@ -58,11 +64,14 @@ export class OcchabFormService { habitats: this._fb.array([]), }); stationForm.setValidators([ - this._formService.dateValidator(stationForm.get('date_min'), stationForm.get('date_max')), + this._formService.dateValidator( + stationForm.get("date_min"), + stationForm.get("date_max") + ), this._formService.minMaxValidator( - stationForm.get('altitude_min'), - stationForm.get('altitude_max'), - 'invalidAlt' + stationForm.get("altitude_min"), + stationForm.get("altitude_max"), + "invalidAlt" ), ]); @@ -71,8 +80,9 @@ export class OcchabFormService { patchDefaultNomenclaureStation(defaultNomenclature) { this.stationForm.patchValue({ - id_nomenclature_area_surface_calculation: defaultNomenclature['METHOD_CALCUL_SURFACE'], - id_nomenclature_geographic_object: defaultNomenclature['NAT_OBJ_GEO'], + id_nomenclature_area_surface_calculation: + defaultNomenclature["METHOD_CALCUL_SURFACE"], + id_nomenclature_geographic_object: defaultNomenclature["NAT_OBJ_GEO"], }); } @@ -83,12 +93,14 @@ export class OcchabFormService { nom_cite: null, habref: [Validators.required, this.cdHabValidator], id_nomenclature_determination_type: defaultNomenclature - ? defaultNomenclature['DETERMINATION_TYP_HAB'] + ? defaultNomenclature["DETERMINATION_TYP_HAB"] : null, determiner: null, id_nomenclature_community_interest: null, id_nomenclature_collection_technique: [ - defaultNomenclature ? defaultNomenclature['TECHNIQUE_COLLECT_HAB'] : null, + defaultNomenclature + ? defaultNomenclature["TECHNIQUE_COLLECT_HAB"] + : null, Validators.required, ], recovery_percentage: null, @@ -100,10 +112,16 @@ export class OcchabFormService { } technicalValidator(habForm: AbstractControl): { [key: string]: boolean } { - const technicalValue = habForm.get('id_nomenclature_collection_technique').value; - const technicalPrecision = habForm.get('technical_precision').value; + const technicalValue = habForm.get( + "id_nomenclature_collection_technique" + ).value; + const technicalPrecision = habForm.get("technical_precision").value; - if (technicalValue && technicalValue.cd_nomenclature == '10' && !technicalPrecision) { + if ( + technicalValue && + technicalValue.cd_nomenclature == "10" && + !technicalPrecision + ) { return { invalidTechnicalValues: true }; } return null; @@ -129,7 +147,10 @@ export class OcchabFormService { addNewHab() { const currentHabNumber = this.stationForm.value.habitats.length - 1; const habFormArray = this.stationForm.controls.habitats as UntypedFormArray; - habFormArray.insert(0, this.initHabForm(this._storeService.defaultNomenclature)); + habFormArray.insert( + 0, + this.initHabForm(this._storeService.defaultNomenclature) + ); this.currentEditingHabForm = 0; } @@ -151,8 +172,11 @@ export class OcchabFormService { */ cancelHab() { if (this.currentEditingHabForm !== null) { - const habArrayForm = this.stationForm.controls.habitats as UntypedFormArray; - habArrayForm.controls[this.currentEditingHabForm].setValue(this.currentHabCopy); + const habArrayForm = this.stationForm.controls + .habitats as UntypedFormArray; + habArrayForm.controls[this.currentEditingHabForm].setValue( + this.currentHabCopy + ); this.currentHabCopy = null; this.currentEditingHabForm = null; } @@ -185,8 +209,8 @@ export class OcchabFormService { this._gn_dataSerice.getGeoInfo(geom).subscribe( (data) => { this.stationForm.patchValue({ - altitude_min: data['altitude']['altitude_min'], - altitude_max: data['altitude']['altitude_max'], + altitude_min: data["altitude"]["altitude_min"], + altitude_max: data["altitude"]["altitude_max"], }); }, () => { @@ -211,7 +235,7 @@ export class OcchabFormService { */ formatNomenclature(obj) { Object.keys(obj).forEach((key) => { - if (key.startsWith('id_nomenclature') && obj[key]) { + if (key.startsWith("id_nomenclature") && obj[key]) { obj[key] = obj[key].id_nomenclature; } }); @@ -232,30 +256,39 @@ export class OcchabFormService { ...hab, id_nomenclature_determination_type: this.getOrNull( hab, - 'nomenclature_determination_method' + "nomenclature_determination_method" ), id_nomenclature_collection_technique: this.getOrNull( hab, - 'nomenclature_collection_technique' + "nomenclature_collection_technique" + ), + id_nomenclature_abundance: this.getOrNull( + hab, + "nomenclature_abundance" ), - id_nomenclature_abundance: this.getOrNull(hab, 'nomenclature_abundance'), }; }); station.habitats.forEach((hab, index) => { - formatedHabitats[index]['habref'] = hab.habref || {}; - formatedHabitats[index]['habref']['search_name'] = hab.nom_cite; + formatedHabitats[index]["habref"] = hab.habref || {}; + formatedHabitats[index]["habref"]["search_name"] = hab.nom_cite; }); - station['habitats'] = formatedHabitats; + station["habitats"] = formatedHabitats; return { ...station, date_min: this._dateParser.parse(station.date_min), date_max: this._dateParser.parse(station.date_max), - id_nomenclature_geographic_object: this.getOrNull(station, 'nomenclature_geographic_object'), + id_nomenclature_geographic_object: this.getOrNull( + station, + "nomenclature_geographic_object" + ), id_nomenclature_area_surface_calculation: this.getOrNull( station, - 'nomenclature_area_surface_calculation' + "nomenclature_area_surface_calculation" + ), + id_nomenclature_exposure: this.getOrNull( + station, + "nomenclature_exposure" ), - id_nomenclature_exposure: this.getOrNull(station, 'nomenclature_exposure'), }; } @@ -269,6 +302,9 @@ export class OcchabFormService { const formatedData = this.formatStationAndHabtoPatch(oneStation.properties); this.stationForm.patchValue(formatedData); + this.stationForm.patchValue({ + observers: oneStation.properties.observers[0], + }); this.stationForm.patchValue({ geom_4326: oneStation.geometry, }); @@ -278,11 +314,13 @@ export class OcchabFormService { /** Format a station before post */ formatStationBeforePost(): StationFeature { let formData = Object.assign({}, this.stationForm.value); + let observers = formData.observers; + formData.observers = [observers]; //format cd_hab formData.habitats.forEach((element) => { if (element.habref) { element.cd_hab = element.habref.cd_hab; - delete element['habref']; + delete element["habref"]; } }); @@ -299,10 +337,10 @@ export class OcchabFormService { }); // Format data in geojson - const geom = formData['geom_4326']; - delete formData['geom_4326']; + const geom = formData["geom_4326"]; + delete formData["geom_4326"]; return { - type: 'Feature', + type: "Feature", geometry: { ...geom, }, diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 09528ce7e8..061b37101b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,82 +1,95 @@ CHANGELOG ========= -2.15.0 (unreleased) -------------------- +2.15.0 - Pavo cristatus 🦚 (unreleased) +--------------------------------------- + +**⏩ En bref** -- Nouvelle version (2.0.0) et déplacement de TaxHub dans GeoNature -- Le module d'import est maintenant intégré dans GeoNature -- La fiche taxon a été revu -- +- Nouvelle version de TaxHub (V2.0.0) intégrée à GeoNature +- Fiche de taxon enrichie intégrant l'actuel profil mais aussi une synthèse géographique, les informations taxonomiques ainsi qu'une liste des statuts de protection +- Module Import intégré à GeoNature +- Import de données vers le module Occhab +- Dernières discussions listées sur la page d'accueil **🚀 Nouveautés** -- [TaxHub] Intégration de TaxHub ([2.0 Release Note](https://github.com/PnX-SI/TaxHub/releases/tag/2.0.0)) à GeoNature (#3150) - - Plus besoin d'un web-service dédiée, la gestion de TaxHub est maintenant intégré à -- [Import] Refonte et intégration du module d'import dans GeoNature (#2833). - - Ajout de l'import vers OccHab - - Possibilité d'importer les données dans plusieurs modules (ou Destination). Suivre la documentation dédiée à ce sujet (mettre lien). - - Evolution des permissions : la création d'un import dépend d'un C dans IMPORT et d'un C dans le module de destination (synthese et/ou occhab) (Voir documention ) - - Plusieurs améliorations sur : les contrôles des données, la génération du rapport, les graphiques produits, de nouveaux tests frontends, etc. -- [Authentification] Possibilité de se connecter à GeoNature avec d'autres fournisseurs d'identité (#3111, https://github.com/PnX-SI/UsersHub-authentification-module/pull/93) - - Plusieurs protocoles de connexions intégrés : OAuth, CAS INPN, UserHub - - Possibilité de se connecter sur d'autres GeoNature - - Voir la documentation pour plus de détails (ajouter lien) -- [Synthèse] Evolution de la fiche taxon (#3191, #3205, #3174,#3175) - - Affichage du profil d'un taxon +- [TaxHub] Intégration de TaxHub ([2.0.0 Release Note](https://github.com/PnX-SI/TaxHub/releases/tag/2.0.0)) à GeoNature (#3150) + - La gestion des taxons est maintenant intégrée dans le module Admin de GeoNature. +- [Import] Refonte et intégration du module Import dans GeoNature (#3269) + - Ajout d'une nouvelle destination : Occhab + - Ajout de la possibilité d'étendre les destinations disponibles pour l'import de données. Suivre la [documentation](https://docs.geonature.fr/development.html#integrer-limport-de-donnees-dans-votre-module) dédiée à ce sujet + - Ajout d'un bouton pour importer des données directement depuis le module de destination (Synthèse et Occhab actuellement) + - Evolution des permissions : la création d'un import dépend de l'action C sur le module Import et de l'action C dans le module de destination (Synthèse et/ou Occhab) + - Plusieurs améliorations : de nouveaux contrôles des données, un rapport d'import revu, intégration de nouveaux tests frontends ... + - Intégration de la documentation du module dans la documentation de GeoNature (Admin, utilisateur, XXXXX URL) +- [Authentification] Possibilité de se connecter à GeoNature avec d'autres fournisseurs d'identité (#3111) + - Plusieurs protocoles de connexion intégrés activables et paramétrables : OAuth, CAS INPN, UserHub + - Possibilité de se connecter sur d'autres instances GeoNature + - Voir la documentation pour plus de détails (XXXX ajouter lien) +- [Synthèse] Evolution de la fiche taxon (#2981, #3131, #3187, #3175) - Affichage de la synthèse géographique d'un taxon - Affichage du statut de protection du taxon - - Affichage des informations taxonomiques présentes dans TaxRef - -- [Synthèse] Possibilité de partager une URL menant directement à un onglet (détails, taxonomie, discussion, validation, etc.) de la fiche d'une observation (#3169) -- [Accueil] Ajout d'un bloc `Discussions` sur la page d'accueil (#3138) - - Affichage des discussions dans lesquels l'utilisateur participé, fait l'enregistrement ou est à l'orgine de l'observation. -- [Occhab] Remplacement du champ `is_habitat_complex` par le nouveau champ du standart `id_nomenclature_type_habitat` (voir MosaiqueValue dans la version 2 standard INPN) (#3125) -- [Occhab] Affichage de l'UUID d'une station dans l'interface (#3247) -- [Méta-données]Il est maintenant possible de supprimer un cadre d'acquisition vide (#3224) + - Affichage des informations taxonomiques présentes dans Taxref + - Ajout d'un lien vers la fiche du taxon dans la liste des observations de la Synthèse (#2718) +- [Synthèse] Possibilité de partager une URL de redirection vers un onglet (détails, taxonomie, discussion, validation, etc.) de la fiche d'une observation (#3169) +- [Accueil] Ajout d'un bloc `Discussions` sur la page d'accueil, désactivable avec le paramètre `DISPLAY_LATEST_DISCUSSIONS` (#3138) + - Filtrable sur uniquement les discussions dans lesquels l'utilisateur authentifié a participé, ou associé à une des observations dont il est : soit l'observateur ou l'opérateur de la saisie (#3194) +- [Occhab] Remplacement du champ `is_habitat_complex` par le nouveau champ `id_nomenclature_type_habitat` et intégration de la nomenclature SINP associée (voir MosaiqueValue dans la version 2 du standard SINP) (#3125) +- [Occhab] Affichage de l'UUID de la station dans sa fiche détail (#3247) +- [Occhab] Amélioration de l'export des données en revoyant la vue `pr_occhab.v_export_sinp` (#3122) +- [Métadonnées] Possibilité de supprimer un cadre d'acquisition vide (#1673) - [Occtax] Ajout du nom de lieu dans le détail d'un relevé (#3145) -- [RefGeo] De nouvelles mailles INPN sur la France métropolitaine (2km, 20km, 50km) sont disponibles (https://github.com/PnX-SI/RefGeo/releases/tag/1.5.4): -``` -geonature db upgrade ref_geo_inpn_grids_2@head # Insertion des mailles 2x2km métropole, fournies par l’INPN -geonature db upgrade ref_geo_inpn_grids_20@head # Insertion des mailles 20x20km métropole, fournies par l’INPN -geonature db upgrade ref_geo_inpn_grids_50@head # Insertion des mailles 50x50km métropole, fournies par l’INPN -``` +- [RefGeo] De nouvelles mailles INPN sur la France métropolitaine (2km, 20km, 50km) sont disponibles (https://github.com/PnX-SI/RefGeo/releases/tag/1.5.4) +- [Monitoring] Ajout de la gestion de la table `gn_monitoring.t_observations` directement dans GeoNature (#2824) **🐛 Corrections** - Correction de l'URL des modules externes dans le menu latéral (#3093) - Correction des erreurs d'exécution de la commande `geonature sensitivity info` (#3216) -- Correction du placement des tooltips pour le composant `ng-select`(#3142) +- Correction du placement des tooltips pour le composant `ng-select` (#3142) +- Correction de l'interrogation des profils dans Occtax (#3156) +- Correction de l'affichage du lieu dans les fiches des relevés Occtax (#3145) - Correction de l'export Occhab avec des champs additionnels vides (#2837) -- Correction du bug d'édition d'une géométrie sur une carte Leaflet (#3196) +- Correction d'un soucis de duplication des géométries quand on modifie un polygone (#3195) +- Correction de la recherche avancée par zonage dans le module Métadonnées (#3250) +- Correction d'un scroll non souhaité dans l'interface du module Synthèse (#3233) +- Correction de l'affichage des acteurs dans les fiches des observations de la Synthèse (#3086) +- Correction du chargement des champs additionnels de type Nomenclature (#3082) +- Correction des filtres taxonomiques avancés dans le mdoule Synthèse (#3087) +- Correction de l'affichage des boutons radio quand ceux-ci sont obligatoires (#3210) +- Correction de la commande `geonature sensitivity info` (#3208) +- Correction de la redirection vers la page d'authentification quand on accède au module Admin sans être authentifié (#3171) +- Correction du scroll du menu latéral dans le module Admin (#3145) +- Correction de l'aperçu des médias de type PDF (#3260) +- Corrections diverses de la documentation +- Ajout d'un action Github permettant de lancer les tests depuis des modules externes (#3232) - Lancement de `pytest` sans _benchmark_ ne nécessite plus l'ajout de `--benchmark-skip` (#3183) - - **⚠️ Notes de version** Si vous mettez à jour GeoNature : -- L'application TaxHub a été integrée dans le module Admin de GeoNature (voir documentation TH) et accessible depuis le menu latéral : - - Les permissions basées sur les profils 1-6 ont été rapatriées et adaptées dans le modèle de permissions de GeoNature. - TaxHub est désormais un "module" GeoNature et dispose des objets de permissions `TAXONS`, `THEMES`, `LISTES` et `ATTRIBUTS` (voir doc GeoNature pour la description des objets). Les personnes ayant anciennement des droits 6 dans TaxHub ont toutes les permissions sur les objets pré-cités. Les personnes ayant des droits inférieurs à 6 et ayant un compte sur TaxHub ont maintenant des permissions sur l'objet `TAXON` (voir et éditer des taxons = ajouter des médias et des attributs) - - L'API de Taxhub est désormais disponible à l'URL `/api/taxhub/api>` (le dernier /api est une rétrocompatibilité et sera enlevé de manière transparante dans les prochaines versions) +- Si vous utilisez le module Monitoring, mettez le à jour en version 1.0.0 minimum en même temps que vous mettez à jour GeoNature +- L'application TaxHub a été integrée dans le module Admin de GeoNature et accessible depuis le menu latéral : + - Les permissions basées sur les profils 1-6 ont été rapatriées et adaptées au modèle de permissions de GeoNature. + TaxHub est désormais un "module" GeoNature et dispose des objets de permissions `TAXONS`, `THEMES`, `LISTES` et `ATTRIBUTS`. Les utilisateurs ayant anciennement des droits 6 dans TaxHub ont toutes les permissions sur les objets pré-cités. Les personnes ayant des droits inférieurs à 6 et ayant un compte sur TaxHub ont maintenant des permissions sur l'objet `TAXON` (voir et éditer des taxons = ajouter des médias et des attributs) + - L'API de TaxHub est désormais disponible à l'URL `/api/taxhub/api>` (le dernier /api est une rétrocompatibilité et sera enlevé de manière transparente dans les prochaines versions) - Le paramètre `API_TAXHUB` est désormais obsolète (déduit de `API_ENDPOINT`) et peut être retiré du fichier de configuration de GeoNature - Si vous utilisez Occtax-mobile, veillez à modifier le paramètre `taxhub_url` du fichier `/geonature/backend/media/mobile/occtax/settings.json`, pour mettre la valeur `/api/taxhub>` - - Une redirection Apache automatique de l'URL de TaxHub et des médias est disponible à l'adresse suivante : XXXX - Les médias ont été déplacés du dossier `/static/medias/` vers `/media/taxhub/`. - Les URL des images vont donc changer. Pour des questions de rétrocompatibilité avec d'autres outils (GeoNature-atlas ou GeoNature-citizen par exemple), vous pouvez définir des règles de redirection pour les médias dans le fichier de configuration Apache de TaxHub : - ``` - # Cas où TaxHub et GeoNature sont sur le même sous-domaine - RewriteEngine on - RewriteRule "^/taxhub/static/medias/(.+)" "/geonature/api/medias/taxhub/$1" [R,L] - # Cas où TaxHub et GeoNature ont chacun un sous-domaine - RewriteEngine on - RewriteRule "^/static/medias/(.+)" "https://geonature./api/medias/taxhub/$1" [R,L] - ``` + Les URL des images vont donc changer. Pour des questions de rétrocompatibilité avec d'autres outils (GeoNature-atlas ou GeoNature-citizen par exemple), vous pouvez définir des règles de redirection pour les médias dans le fichier de configuration Apache de TaxHub : + ``` + # Cas où TaxHub et GeoNature sont sur le même sous-domaine + RewriteEngine on + RewriteRule "^/taxhub/static/medias/(.+)" "/geonature/api/medias/taxhub/$1" [R,L] + # Cas où TaxHub et GeoNature ont chacun un sous-domaine + RewriteEngine on + RewriteRule "^/static/medias/(.+)" "https://geonature./api/medias/taxhub/$1" [R,L] + ``` - L'application TaxHub n'est plus nécessaire, si vous voulez utilisez TaxHub uniquement au travers de GeoNature, effectuer les actions suivantes : - Suppression de la branche alembic taxhub : `geonature db downgrade taxhub-standalone@base` - - Les commandes de taxhub sont maitenant intégrées dans celles de GeoNature. + - Les commandes de TaxHub sont maintenant acessibles depuis la commande `geonature` ```shell geonature taxref info # avant flask taxref info geonature taxref enable-bdc-statut-text # avant flask taxref enable-bdc-statut-text @@ -84,20 +97,30 @@ Si vous mettez à jour GeoNature : ``` - L'intégration de TaxHub dans GeoNature entraine la suppression du service systemd et la conf apache spécifique à TaxHub. Les logs de TH sont également centralisés dans le fichier de log de GeoNature - - **⚠️Important⚠️** ! Ajouter l'extension `ltree` à votre base de données : `sudo -n -u postgres -s psql -d $db_name -c "CREATE EXTENSION IF NOT EXISTS ltree;"` + - **⚠️Important⚠️** ! Ajouter l'extension `ltree` à votre base de données : `sudo -n -u postgres -s psql -d -c "CREATE EXTENSION IF NOT EXISTS ltree;"` - Le module Import a été intégré dans le coeur de GeoNature - - si vous aviez installé le module externe Import, XXXXX - - si vous n'aviez pas installé le module externe Import, il sera disponible après la mise à jour vers cette nouvelle version de GeoNature. Vous pouvez configurer les permissions de vos utilisateurs si vous souhaitez qu'ils y accédent - - la gestion des permissions et des JDD associés aux module a évolué. La migration est gérée automatiquement lors de la mise à jour pour garantir un fonctionnement identique. Voir la documentation (XXXXXXXXX) pour en savoir plus - - supprimer le dossier import, il ne sera plus utiliser dans la 2.15 - - reporter la configuration IMPORT dans le fichier de configuration de GeoNature. (dans le bloc import, voir dans le fichier default toml) + - Si vous aviez installé le module externe Import, l'ancienne version sera désinstallée lors de la mise à jour de GeoNature. + - Si vous n'aviez pas installé le module externe Import, il sera disponible après la mise à jour vers cette nouvelle version de GeoNature. Vous pouvez configurer les permissions de vos utilisateurs si vous souhaitez qu'ils y accédent. + - La gestion des permissions et des JDD associés aux module a évolué. La migration est gérée automatiquement lors de la mise à jour pour garantir un fonctionnement identique. + - Reporter l'éventuelle configuration de votre module Import dans le fichier de configuration de GeoNature (dans le bloc import, voir dans le fichier default toml) - La synchronisation avec le service MTD de l'INPN n'est plus intégrée dans le code de GeoNature, elle a été déplacée dans un module externe : https://github.com/PnX-SI/mtd_sync - Si vous l'utilisiez, supprimer les variables de configuration suivantes du fichier `geonature_config.toml` : - `XML_NAMESPACE`, `MTD_API_ENDPOINT` - toutes les variables dans `[CAS_PUBLIC]`, `[CAS]`, `[CAS.CAS_USER_WS]`, `[MTD]` - `ID_USER_SOCLE_1` et `ID_USER_SOCLE_2` dans la section `BDD` +- Si vous utilisez le module Monitoring, n'oubliez pas de mettre à jour ce dernier sur la version 1.0. +- Si vous utilisez le module Monitoring, les champs `id_digitizer` des tables `gn_monitoring.t_base_sites`, `gn_monitoring.t_base_visits` est désormais obligatoire. Assurez-vous qu'ils soient peuplés avant de lancer la mise à jour de GeoNature (`SELECT * FROM gn_monitoring.t_base_visits tbv WHERE id_digitiser IS NULL; SELECT * FROM gn_monitoring.t_base_sites tbs WHERE id_digitiser IS NULL;`). +- Si vous souhaitez intégrer les nouvelles mailles INPN : + ``` + geonature db upgrade ref_geo_inpn_grids_2@head # Insertion des mailles 2x2km métropole, fournies par l’INPN + geonature db upgrade ref_geo_inpn_grids_20@head # Insertion des mailles 20x20km métropole, fournies par l’INPN + geonature db upgrade ref_geo_inpn_grids_50@head # Insertion des mailles 50x50km métropole, fournies par l’INPN + ``` + +**📝 Merci aux contributeurs** +@amandine-sahl, @Pierre-Narcisi, @jacquesfize, @TheoLechemia, @bouttier, @andriacap, @edelclaux, @JulienCorny, @VincentCauchois, @CynthiaBorotPNV, @JeromeMaruejouls, @jbrieuclp, @blaisegeo, @lpofredc, @amillemonchicourt, @ch-cbna 2.14.2 (2024-05-28) ------------------- diff --git a/docs/admin-manual.rst b/docs/admin-manual.rst index 4c1690fd39..6eaf79eeec 100644 --- a/docs/admin-manual.rst +++ b/docs/admin-manual.rst @@ -1,4 +1,4 @@ -MANUEL ADMINISTRATEUR +Manuel administrateur ===================== Architecture @@ -1216,6 +1216,7 @@ propriété ``label``. Un exemple est disponible ci-dessous. label = "Mot de passe oublié ?" url = "https://mon.formulaire.de.motdepasse" + Customiser l'aspect esthétique """""""""""""""""""""""""""""" @@ -1239,28 +1240,28 @@ Autre exemple, il est possible personnaliser les polices ou les couleurs : .. code-block:: css -/* IMPORT POLICE BEBAS NEUE - * ! Bebas Neue by @Ryoichi Tsunekawa - * License - Open Font License - */ -@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap'); -/* Couleurs principales de l'application */ -.color-blue { - color:#678BC5!important; -} -.color-orange { - color:#DEC70D!important; -} -.color-teal { - color:#A8DE0D!important; -} -.color-red { - color:#DE280D!important -} -#appName h3{ - font-family:Bebas Neue,Arial,sans-serif!important; - font-size: xx-large -} + /* IMPORT POLICE BEBAS NEUE + * ! Bebas Neue by @Ryoichi Tsunekawa + * License - Open Font License + */ + @import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap'); + /* Couleurs principales de l'application */ + .color-blue { + color:#678BC5!important; + } + .color-orange { + color:#DEC70D!important; + } + .color-teal { + color:#A8DE0D!important; + } + .color-red { + color:#DE280D!important + } + #appName h3{ + font-family:Bebas Neue,Arial,sans-serif!important; + font-size: xx-large + } Certains paramètres demandent l'ajout de la mention ``!important`` pour être pris en compte (https://github.com/PnX-SI/GeoNature/issues/2632). @@ -2445,4 +2446,4 @@ Les permissions du module TaxHub peuvent être reglées aux trois niveaux (objet - ATTRIBUTS : permet de voir / créer / modifier / supprimer des attributs de taxons -.. include:: import_doc.rst +.. include:: admin/import_admin.rst diff --git a/docs/admin/authentication_custom.rst b/docs/admin/authentication-custom.rst similarity index 100% rename from docs/admin/authentication_custom.rst rename to docs/admin/authentication-custom.rst diff --git a/docs/admin/import-admin.rst b/docs/admin/import-admin.rst new file mode 100644 index 0000000000..c942e650df --- /dev/null +++ b/docs/admin/import-admin.rst @@ -0,0 +1,297 @@ + +Module Import +--------------- + +Ce module permet d’importer des données depuis un fichier CSV dans GeoNature. + +Concepts +"""""""" + +**Destination.** Une destination est déterminée par le module dans lequel on souhaite importer des données (e.g. Occhab, Synthèse, etc.). + +**Entités.** Une entité correspond à un objet dans une destination (e.g. *station* est une entité de la destination *Occhab*) + + +Faire un import, le minimum requis +"""""""""""""""""""""""""""""""""" + +Pour qu’un utilisateur puisse mener au bout un import, il doit posséder à minima les permissions suivantes : + +* Création d’un import (C) +* Voir les imports (R) +* Voir des mappings (R) +* Droit de créer/saisir dans le module de destination (C dans OccHab par ex.) + +**Jeu de données.** Un import s’effectue dans un jeu de données, par conséquent, ce dernier doit : + +- être associé aux modules de destination de l’import (Voir champ dans l’édition/création d’un JDD : Module(s) GeoNature associé(s) au jeu de données) +- être actif. + + +Déroulement d’un import +""""""""""""""""""""""" + +Dans le module d’import, trois actions sont possibles : la création d’un import, la modification de ce dernier et la suppression d’un import. +Lors du lancement de la création d’un import, il faut sélectionner la destination. Une fois la destination choisie, la phase de préparation de l’import se déroule de la manière suivante : + +1. Téléverser le fichier contenant les données et sélection du jeu de données. Le format de fichier accepté est le CSV. +2. Définir les paramètres de lecture du fichier téléversé. Les données du fichier source sont stockées en binaire dans la table des imports (``gn_imports.t_imports.source_file``). +3. Faire correspondre les colonnes de votre fichier avec les champs du modèle de données défini dans le module de destination. Pour aider l'utilisateur dans le remplissage du formulaire, il est possible de sauvegarder et réutiliser des _mappings_. Plusieurs mappings sont disponibles avec l'installation de GeoNature. Ces mappings permettent notamment de faire la correspondance des colonnes d'un fichier produit par un export GeoNature (Occhab et Synthèse). Les correspondances de champs sont stockées dans un champs JSON dans ``gn_imports.t_imports.fieldmapping``. +4. Si des champs correspondant à des types de nomenclatures sont indiqués dans l'étape 3, une mise en correspondance des valeurs du fichier source avec les nomenclatures dans la base doit être faite. Si le fichier source comprend des lignes vides, on propose en plus de mapper le cas "Pas de valeur". Tout comme la correspondance des champs, la correspondance des valeurs de nomenclature est sauvegardée dans un champs JSONB ``gn_imports.t_imports.valuemapping``. +5. Contrôles des données du fichier source sélectionnées (:ref:`c.f. Contrôles de données`:). + +Une fois, cette phase de préparation terminée, l’utilisateur se voit présenter les données jugées comme valides (resp. les données invalides). A cette étape, l’utilisateur a la choix de modifier les données invalides dans son fichier source et recommencer le processus de préparation de l’import OU lancer l’import des données dans la destination. +Une fois l’import de données terminé, l’utilisateur est redirigé vers un rapport récapitulant les paramètres de l’import et un affichage de quelques statistiques spécifiques au type de données importées. +Une fois les données importées, les données sont supprimées de la table temporaire (``gn_imports.t_imports_synthese`` pour la Synthèse, ``gn_imports.t_imports_occhab`` pour Occhab). + + +.. image:: images/import/import_etapes.png + +Configuration du module d’import +"""""""""""""""""""""""""""""""" + +Vous pouvez surcoucher ces différents paramètres en les ajoutant directement dans le fichier de configuration principal de GeoNature (``geonature_config.toml``). + + ============================================== ============================================================================================================================================================================ + Variable Description + ============================================== ============================================================================================================================================================================ + ENCODAGE Liste des encodages + acceptés + INSTANCE_BOUNDING_BOX Zone rectangulaire dans laquelle les données doivent être localisées + ENABLE_BOUNDING_BOX_CHECK Activer la vérification de l'appartenance des données importées à la bounding box définie dans INSTANCE_BOUNDING_BOX + ENABLE_SYNTHESE_UUID_CHECK Activer la vérification des UUID dans les données importées avec ceux de la Synthèse + MAX_FILE_SIZE Taille maximale du fichier chargé (en Mo) + SRID SRID autorisés pour les fichiers en entrée + ALLOWED_EXTENSIONS Extensions autorisées (seul le format CSV est accepté actuellement) + ALLOW_VALUE_MAPPING Activer ou non l'étape du mapping des valeurs + DEFAULT_VALUE_MAPPING_ID Si le mapping des valeurs est désactivé, specifier l'identifiant du mapping qui doit être utilisé + FILL_MISSING_NOMENCLATURE_WITH_DEFAULT_VALUE Rempli les valeurs de nomenclature erronées par la valeur par défaut + DISPLAY_CHECK_BOX_MAPPED_FIELD Afficher le bouton pour afficher/masquer les champs déjà mappés + CHECK_PRIVATE_JDD_BLURING Active la vérification de l'existence du champs "floutage" si le JDD est privé + CHECK_REF_BIBLIO_LITTERATURE Active la vérification de la référence bibliographique fournie si la valeur du champs source = "litterature" + CHECK_EXIST_PROOF Active la vérification qu'une preuve d'existence est fournie si preuve existence = "oui" + EXPORT_REPORT_PDF_FILENAME Customiser le nom du fichier de rapport de l'import + DEFAULT_RANK Paramètre pour définir le rang utilisé pour le diagramme camembert du rapport d'import. + DEFAULT_GENERATE_MISSING_UUID L'UUID d'une entité importée sera généré s'il n'est pas indiqué dans le fichier source + ID_AREA_RESTRICTION Identifiant d'une géométrie présente dans RefGeo. Si différent de -1, vérifie si les géométries des entités importées sont bien dans l'emprise spatiale de cette dernière. + ID_LIST_TAXA_RESTRICTION Identifiant d'une liste de taxons permettant de restreindre l'import d'observations dont les taxons appartiennent à cette dernière + MODULE_URL URL d'accès au module d'import + DATAFRAME_BATCH_SIZE Taille des `batch` de données importées en même temps + ============================================== ============================================================================================================================================================================ + + +Permissions de l’import +""""""""""""""""""""""" + +Dans le module Import, il existe le jeu de permissions suivant : + +* Création d’un import – C +* Voir les imports – R +* Modifier des imports – U (nécessaire d’avoir le C) +* Supprimer des imports – D +* Créer des mappings - C +* Voir des mappings - R +* Modifier des mappings - U +* Supprimer des mappings - D + +**Scope.** Similaire à d’autres permissions dans GeoNature, il est possible de limiter l’accès à l’utilisateur sur les données sur lesquelles il peut agir. L’ ajout de scope sur une permission de l’import limite la visibilité des imports dans l’interface « Liste des Imports » ainsi que la possibilité (resp. impossbilité) de modifier ou supprimer un import. Par exemple, un R2 sur « IMPORT » permet uniquement de voir les imports effectués par soi-même ou un utilisateur de son organisme. +A noter! La liste des jeux de données disponibles s’appuie bien sur les permissions de l’utilisateur dans ce dernier ! + +**Mapping.** Certains mappings sont définis comme "public" et sont accessibles à tout le monde. Seuls les administrateurs (U=3) et les propriétaires de ces mappings peuvent les modifier. Si vous modifiez un mapping sur lequel vous n'avez pas les droits, il vous sera proposé de créer un nouveau mapping vous appartenant avec les modifications que vous avez faites, mais sans modifier le mapping initial. + +**Jeu de données accessibles à l'import.** Les jeux de données selectionnables par un utilisateur lors de la création d'un import sont eux controlés par les permissions sur le C de l'objet "import" (combiné au R du module "Métadonnées). Les mappings constituent un "objet" du module d'import disposant de droits paramétrables pour les différents utilisateurs, indépendamment des permissions sur les imports. Le réglage des permissions se fait dans le module "Admin" de GeoNature ("Admin" -> "Permissions"). + + +Modification et Suppression d'un import +""""""""""""""""""""""""""""""""""""""" + +**Comment sait-on qu'un import est terminé ?** Si une date apparait dans la colonne "Fin import" de la liste des imports, alors l'import est terminé. + +**Suppression d'un import** La suppression d'un import implique : la supression de l'import (l'objet) et **les données importées dans la table transitoire**. Si l'import est terminé, les données importées dans la destination sont supprimées. Dans le cas d'une destination avec plusieurs entités, si l'entité mère est associée à des entités filles ajoutées en dehors de l'import (un habitat est rajouté sur un station importée par exemple), la supression est bloquée. + +**Modification d'un import** Lors de la modification d'un import, vous serez redirigez vers l'étape de "Correspondances de champs". Si vous modifiez la correspondance des champs en cliquant sur "Suivant", cela entrainera la suppression des données dans la table transitoire et dans la destination si l'import est terminé. + +Contrôles de données +"""""""""""""""""""" + +**Erreurs** + + =================================== ============================================================================================================================================================================================================================================================================================================== + Code Erreur Description + =================================== ============================================================================================================================================================================================================================================================================================================== + DATASET_NOT_FOUND L’identifiant ne correspond à aucun jeu de données existant. + DATASET_NOT_AUTHORIZED L’utilisateur ne peut pas importer de nouvelles entités dans le jeu de données. + DATASET_NOT_ACTIVE Aucune donnée ne peut être importée dans le JDD indiqué car il n’est pas actif. + MULTIPLE_ATTACHMENT_TYPE_CODE Plusieurs géoréférencements sont indiqués dans les colonnes : codeCommune, codeMaille, codeDépartement (Erreur Synthèse) + MULTIPLE_CODE_ATTACHMENT Plusieurs codes de rattachement fournis pour une même ligne. Une ligne doit avoir un seul code rattachement (code commune OU code maille OU code département) + INVALID_DATE Format de date invalide (Voir formats de date autorisés) + INVALID_UUID Format de l’identifiant donné ne respecte pas le format UUID (https://fr.wikipedia.org/wiki/Universally_unique_identifier) + INVALID_INTEGER La donnée indiquée ne correspond pas un nombre entier. + INVALID_NUMERIC La donnée indiquée ne correspond pas à un nombre réel (float) + INVALID_WKT La donnée indiquée ne respecte pas le format WKT https://fr.wikipedia.org/wiki/Well-known_text + INVALID_GEOMETRY La géométrie de la donnée renseignée est invalide (c.f  ST_VALID) + INVALID_BOOL La donnée fournie n’est pas un booléen + INVALID_ATTACHMENT_CODE Le code commune/maille/département indiqué ne fait pas partie du référentiel des géographique. + INVALID_CHAR_LENGTH La chaine de caractère de la donnée est trop longue + DATE_MIN_TOO_HIGH La date de début est dans le futur + DATE_MAX_TOO_LOW La date de fin est inférieure à 1900 + DATE_MAX_TOO_HIGH La date de fin est dans le futur + DATE_MIN_TOO_LOW La date de début est inférieure à 1900 + DATE_MIN_SUP_DATE_MAX La date de début est supérieure à la date de fin + DEPTH_MIN_SUP_ALTI_MAX La profondeur minimum est supérieure à la profondeur maximale + ALTI_MIN_SUP_ALTI_MAX L’altitude minimum est supérieure à l’altitude maximale + ORPHAN_ROW La ligne du fichier n’a pû être rattachée à aucune entité. + DUPLICATE_ROWS Deux lignes du fichier sont identiques ; les lignes ne peuvent pas être dupliquées. + DUPLICATE_UUID L'identifiant UUID d’une entité n'est pas unique dans le fichier fournis + EXISTING_UUID L'identifiant UUID d’une entité fournie existe déjà dans la base de données. Il faut en fournir un autre ou laisser la valeur vide pour une attribution automatique. + SKIP_EXISTING_UUID Les entités existantes selon UUID sont ignorées. + MISSING_VALUE Valeur manquante dans un champs obligatoire + MISSING_GEOM Géoréférencement manquant ; un géoréférencement doit être fourni, c’est à dire qu’il faut livrer : soit une géométrie, soit une ou plusieurs commune(s), ou département(s), ou maille(s), dont le champ “typeInfoGeo” est indiqué à 1. + GEOMETRY_OUTSIDE La géométrie se trouve à l'extérieur du territoire renseigné + NO-GEOM Aucune géometrie fournie (ni X/Y, WKT ou code) + GEOMETRY_OUT_OF_BOX Coordonnées géographiques en dehors du périmètre géographique de l'instance + ERRONEOUS_PARENT_ENTITY L’entité parente est en erreur. + NO_PARENT_ENTITY Aucune entité parente identifiée. + DUPLICATE_ENTITY_SOURCE_PK Deux lignes du fichier ont la même clé primaire d’origine ; les clés primaires du fichier source ne peuvent pas être dupliquées. + COUNT_MIN_SUP_COUNT_MAX Incohérence entre les champs dénombrement. La valeur de denombrement_min est supérieure à celle de denombrement_max ou la valeur de denombrement_max est inférieure à denombrement_min. + INVALID_NOMENCLATURE Code nomenclature erroné ; La valeur du champ n’est pas dans la liste des codes attendus pour ce champ. Pour connaître la liste des codes autorisés, reportez-vous au Standard en cours. + INVALID_EXISTING_PROOF_VALUE Incohérence entre les champs de preuve ; si le champ “preuveExistante” vaut oui, alors l’un des deux champs “preuveNumérique” ou “preuveNonNumérique” doit être rempli. A l’inverse, si l’un de ces deux champs est rempli, alors “preuveExistante” ne doit pas prendre une autre valeur que "oui" (code 1). + INVALID_NOMENCLATURE_WARNING (Non bloquant) Code nomenclature erroné et remplacé par sa valeur par défaut ; La valeur du champ n’est pas dans la liste des codes attendus pour ce champ. Pour connaître la liste des codes autorisés, reportez-vous au Standard en cours. + CONDITIONAL_MANDATORY_FIELD_ERROR Champs obligatoires conditionnels manquants. Il existe des ensembles de champs liés à un concept qui sont “obligatoires conditionnels”, c’est à dire que si l'un des champs du concept est utilisé, alors d'autres champs du concept deviennent obligatoires. + UNKNOWN_ERROR Erreur inconnue + INVALID_STATUT_SOURCE_VALUE Référence bibliographique manquante ; si le champ “statutSource” a la valeur “Li” (Littérature), alors une référence bibliographique doit être indiquée. + CONDITIONAL_INVALID_DATA Erreur de valeur + INVALID_URL_PROOF PreuveNumerique n’est pas une url ; le champ “preuveNumérique” indique l’adresse web à laquelle on pourra trouver la preuve numérique ou l’archive contenant toutes les preuves numériques. Il doit commencer par “http://”, “https://”, ou “ftp://”. + ROW_HAVE_TOO_MUCH_COLUMN Une ligne du fichier source a plus de colonnes que l'en-tête. + ROW_HAVE_LESS_COLUMN Une ligne du fichier source a moins de colonnes que l'en-tête. + EMPTY_ROW Une ligne dans le fichier source est vide + HEADER_SAME_COLUMN_NAME Au moins deux colonnes du fichier source possèdent des noms identiques + EMPTY_FILE Le fichier source est vide + NO_FILE_SENDED Aucun fichier source n’a été téléversé. + ERROR_WHILE_LOADING_FILE Une erreur s’est produite lors du chargement du fichier. + FILE_FORMAT_ERROR Le format du fichier est incorrect. + FILE_EXTENSION_ERROR L'extension de fichier source est incorrect + FILE_OVERSIZE Volume du fichier source est trop important + FILE_NAME_TOO_LONG Nom du fichier de données trop long + FILE_WITH_NO_DATA Pas de données dans le fichier source + INCOHERENT_DATA Une même entité est déclaré avec différents attributs dans le fichier source + CD_HAB_NOT_FOUND CdHab n’existe pas dans le référentiel Habref installé + CD_NOM_NOT_FOUND CdNom n’existe pas dans le référentiel TaxRef installé + =================================== ============================================================================================================================================================================================================================================================================================================== + + +**Format de dates autorisées** + +Date : + +- YYYY-MM-DD +- DD-MM-YYYY +- YYYY/MM/DD +- DD/MM/YYYY + +Heure : + +- H +- H-M +- H-M-S +- H-M-S +- H:M +- H:M:S +- H:M:S +- Hh +- HhM +- HhMm +- HhMmSs + + +Configuration avancée +""""""""""""""""""""" + +Une autre partie de la configuration se fait directement dans la base de données, dans les tables ``bib_fields``, ``bib_themes`` et ``cor_entity_field``. + +Dans ``bib_fields``, il est possible de : + +- Ajouter de nouveau(x) champ(s) pour une entité (e.g. Station) dans une destination (e.g. Occhab). +- Masquer des champs existants. Pour cela, modifier la valeur de l'attribut ``display`` d'un champ. +- Rendre obligatoire un champ. Pour cela, modifier la valeur de l'attribut ``mandatory`` d'un champ. +- Rendre obligatoire/optionnel un champ si d'autres champs sont remplis. Voir les champs ``optional_conditions`` et ``mandatory_conditions``. + +Dans la table ``cor_entity_field`` : + +- Paramètrer l'ordre des champs dans l'interface du mapping de champs. Voir le champ ``order_field``. +- Changer le _tooltip_ d'un champ. Voir le champ ``comment``. +- Regrouper des champs dans **thèmes** (voir ``bib_themes``) à l'aide du champs ``id_theme``. + +.. _controle donnees: + +Contrôle de données dans les destinations venant avec GeoNature +""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" + + +Dans cette section, nous présentons les contrôles de données effectuées pour les destinations intégrées dans GeoNature : Synthèse, Occhab. +L’ordre des contrôles dans ces listes correspond bien à celui du processus défini dans le code de GeoNature. +De manière générale, nous séparons les contrôles de données en deux catégories, ceux effectués en BDD avec PostgreSQL et ceux effectuée en Python à l’aide des DataFrame (donnée tableau) + + +**Listes des contrôles pour Occhab** + + +1. [SQL][Station] + + 1. Vérification de la cohérence des données des stations déclarées + +2. [DataFrame][Station] + + 1. Vérification de l’existence de données pour les champs obligatoires + 2. Vérification de la concordance entre le type d’un champ et la données + 3. Vérification du jeu de données + 4. Vérification des géométries présentes dans les données (WKT ou latitude/longitude) + +3. [DataFrame][Habitat] + + 1. Vérification de l’existence de données pour les champs obligatoires + 2. Vérification de la concordance entre le type d’un champ et la données + +4. [SQL][Station] + + 1. Mapping des valeurs de nomenclatures + 2. Conversion des données de géométrie dans le SRID de la BDD + 3. Vérification de la cohérence des données altitudinale, de profondeur et les dates + 4. Vérification de la validité des géométries + +5. [SQL][Habitat] + + 1. Mapping des valeurs de nomenclatures + 2. Vérification des cdHab + 3. Vérification des UUID (doublons dans le fichier, existence dans la destination) + 4. Générer les UUID si manquante + 5. Dans le cas d’habitats importés sur une station existante, vérifier les droits de l’utilisateur sur cette dernière. + + +**Listes des contrôles pour la Synthèse** + +1. [DataFrame] + + 1. Vérification de l’existence de données pour les champs obligatoires + 2. Vérification de la concordance entre le type d’un champ et la donnée + +2. [SQL] + + 1. Vérification du jeu de données + 2. Vérification des géométries présentes dans les données (WKT ou latitude/longitude) + 3. Vérification des données de dénombrement + 4. Mapping des nomenclatures + 5. Vérification de l’existence des identifiants cdNom dans Taxref local + 6. Vérification de l’existence des identifiants cdHab dans Habref local + 7. Vérification de la cohérence des données altitudinale, de profondeur et les dates + 8. Vérification des preuves numériques + 9. Vérification de l’intersection entre chaque géométrie et la géométrie de la zone autorisée. + + + +Modèle de données +""""""""""""""""" + +Le diagramme ci-dessous présente le schéma de la base de données du module Import. + +.. image:: images/import/import_modele.png diff --git a/docs/authors.rst b/docs/authors.rst index 22d2ceb8ee..474fc726f5 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -1,4 +1,4 @@ -AUTEURS +Auteurs ======= Parc national des Écrins diff --git a/docs/conf.py b/docs/conf.py index 4a63e12b6c..2c16584f8b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,7 +44,7 @@ ## AUTOAPI extensions.append("autoapi.extension") autoapi_dirs = ["../backend/geonature", "../backend/dependencies"] -autoapi_ignore = ["*migrations*", "*tests*"] +autoapi_ignore = ["*migrations*", "*tests*", "*celery_app.py"] autoapi_add_toctree_entry = False @@ -54,7 +54,11 @@ # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # -source_suffix = [".rst", ".md"] +source_suffix = { + ".rst": "restructuredtext", + ".txt": "markdown", + ".md": "markdown", +} # The master toctree document. master_doc = "index" @@ -97,25 +101,33 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "sphinx_rtd_theme" +html_theme = "sphinx_book_theme" html_logo = "./images/LogoGeonature.jpg" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = { - "canonical_url": "", - "logo_only": False, - "display_version": True, - "prev_next_buttons_location": "bottom", - "style_external_links": False, - "style_nav_header_background": "white", - # Toc options - "collapse_navigation": True, - "sticky_navigation": True, "navigation_depth": 4, - "includehidden": True, - "titles_only": False, + "use_source_button": True, + "repository_provider": "github", + "repository_url": "https://github.com/PnX-SI/GeoNature", + "path_to_docs": "docs", + "repository_branch": "master", + "use_repository_button": True, + "collapse_navbar": True, + "icon_links": [ + { + # Label for this link + "name": "GitHub", + # URL where the link will redirect + "url": "https://github.com/PnX-SI/GeoNature", # required + # Icon class (if "type": "fontawesome"), or path to local image (if "type": "local") + "icon": "fa-brands fa-square-github", + # The type of image to be used (see below for details) + "type": "fontawesome", + } + ], } # Add any paths that contain custom static files (such as style sheets) here, @@ -128,7 +140,7 @@ # # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -html_sidebars = {"**": ["globaltoc.html", "relations.html", "sourcelink.html", "searchbox.html"]} +# html_sidebars = {"**": ["globaltoc.html", "relations.html", "sourcelink.html", "searchbox.html"]} # html_sidebars = { # '**': [ # 'relations.html', # needs 'show_related': True theme option to display diff --git a/docs/development.rst b/docs/development.rst index 651e98c7ce..30ba724604 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -1,4 +1,4 @@ -DEVELOPPEMENT +Développement ============= Général @@ -1641,6 +1641,8 @@ Bonnes pratiques Frontend +.. include:: development/import-dev.rst + Documentation ------------- diff --git a/docs/development/import-dev.rst b/docs/development/import-dev.rst new file mode 100644 index 0000000000..7a2b2afc25 --- /dev/null +++ b/docs/development/import-dev.rst @@ -0,0 +1,302 @@ +Intégrer l’import de données dans votre module +---------------------------------------------- + +A partir de la version 2.15, le module d’Import permet l’ajout de nouvelles destinations en plus de la Synthèse. Cela a été l’occasion d’ajouter la possibilité d’importer des données d’habitat dans le module Occhab. +Cette section présente le processus d’ajout de l’import dans votre module GeoNature. + +Modification à apporter sur la base de données +********************************************** + +Plusieurs points sont essentiels au bon fonctionnement de l’import dans votre module : + +1. Avoir une permission C sur votre module de destination +2. Créer un objet destination (`bib_destinations`) et autant d’entités (`bib_entities`) que vous avez d’objets dans votre module (e.g. habitat, station pour Occhab) +3. Créer une table transitoire permettant d’effectuer la complétion et le contrôle des données avant l’import des données vers la table de destination finale. +4. Pour chaque entité, déclarer les attributs rendus accessibles à l’import dans `bib_fields` +5. Si de nouvelles erreurs de contrôle de données doivent être déclarées, ajouter ces dernières dans `bib_errors_type` + +N.B. Comme dans le reste de GeoNature, il est conseillé d’effectuer les modifications de base de données à l’aide de migrations Alembic. + +Permissions requises +"""""""""""""""""""" + +Si ce n'est pas le déjà cas, ajouter la permission de création de données dans votre module. Le code ci-dessous donne un exemple fonctionnant dans une révision alembic. + +.. code-block:: python + + 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 + ('[votreModuleCode', 'ALL', 'C', True, 'Créer [nomEntité]') + ) 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 + """ + ) + + +Créer votre destination et vos entités +"""""""""""""""""""""""""""""""""""""" + +Dans un premier temps, il faut créer une "destination". Pour cela, il faut enregistrer votre module dans `bib_destinations`. + +.. code-block:: python + + # Récupérer l'identifiant de votre module + id_de_votre_module = ( + op.get_bind() + .execute("SELECT id_module FROM gn_commons.t_modules WHERE module_code = 'CODE_DE_VOTRE_MODULE'") + .scalar() + ) + + # Ajouter la destination + # N.B. table_name correspond au nom de la future table transitoire + destination = Table("bib_destinations", meta, autoload=True, schema="gn_imports") + op.get_bind() + .execute( + sa.insert(destination) + .values( + id_module=id_de_votre_module, + code="votre_module_code", + label="VotreModule", + table_name="t_imports_votre_module", + ) + ) + + +Dans votre module, plusieurs objets sont manipulés et stockés chacun dans une table. Pour prendre l'exemple du module Occhab, on a deux entités les stations et les habitats. + +.. code-block:: python + + id_dest_module= ( + op.get_bind() + .execute("SELECT id_destination FROM gn_imports.bib_destinations WHERE code = 'votre_module_code'") + .scalar() + ) + entity = Table("bib_entities", meta, autoload=True, schema="gn_imports") + op.get_bind() + .execute( + sa.insert(entity) + .values( + id_destination=id_dest_module, + code="code_entite1", + label="Entite1", + order=1, + validity_column="entite1_valid", + destination_table_schema="votre_module_schema", + destination_table_name="entite1_table_name", + ) + ) + + + +Créer votre table transitoire +""""""""""""""""""""""""""""" + +Nécessaire pour le contrôle de données, il est important de créer une table transitoire permettant d’effectuer la complétion et le contrôle des données avant l’import des données vers la table de destination finale. La table transitoire doit contenir les colonnes suivantes : + +- id_import : identifiant de l’import +- line_no : entier, numéro de la ligne dans le fichier source +- entityname_valid : booleen, indique si une entité est valide +- pour chaque champ de l'entité, il faudra une colonne VARCHAR contenant la donnée du fichier et une colonne du type du champ qui contiendra la données finales. La convention de nommage est la suivante: "src_nomchamp" pour colonne contenant la données du fichier source et "nomchamp" pour la colonne contenant les données finales. Il est conseillé que le nom de la colonne contenant les données finales soit identiques à celle du champs dans la table de destination. + +.. code-block:: python + + op.create_table( + "t_imports_votremodule", + sa.Column( + "id_import", + sa.Integer, + sa.ForeignKey("gn_imports.t_imports.id_import", onupdate="CASCADE", ondelete="CASCADE"), + primary_key=True, + ), + sa.Column("line_no", sa.Integer, primary_key=True), + sa.Column("entite1_valid", sa.Boolean, nullable=True, server_default=sa.false()), + # Station fields + sa.Column("src_id_entite", sa.Integer), + sa.Column("id_entite", sa.String), + sa.Column("src_unique_dataset_id", sa.String), + sa.Column("unique_dataset_id", UUID(as_uuid=True)), + [...] + ) + + +Déclarer les attributs rendus accessibles à l’import dans **bib_fields** +"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" + +Pour chaque entité (e.g. une station dans Occhab), il faut déclarer les champs du modèles accessibles à l’import dans `bib_fields`. + +.. code-block:: python + + theme = Table("bib_themes", meta, autoload=True, schema="gn_imports") + + id_theme_general = ( + op.get_bind() + .execute(sa.select([theme.c.id_theme]).where(theme.c.name_theme == "general_info")) + .scalar() + ) + + + fields_entities=[ + ( + { + "name_field": "id_entite1", + "fr_label": "Identifiant entité1", + "mandatory": True, # Obligatoire ou non + "autogenerated": False, # généré automatique (ex. UUID) + "display": False, # Afficher dans l'UI + "mnemonique": None, + "source_field": None, + "dest_field": "id_entite1", + }, + { + id_entity1: { # récupérer l'id de l'entité entité1 précédement inséré + "id_theme": id_theme_general, + "order_field": 0, + "comment": "", # Utilisé comme tooltip + }, + }, + ), + ... + ] + + field = Table("bib_fields", meta, autoload=True, schema="gn_imports") + id_fields = [ + id_field + for id_field, in op.get_bind() + .execute( + sa.insert(field) + .values([{"id_destination": id_votre_dest, **field} for field, _ in fields_entities]) + .returning(field.c.id_field) + ) + .fetchall() + ] + cor_entity_field = Table("cor_entity_field", meta, autoload=True, schema="gn_imports") + op.execute( + sa.insert(cor_entity_field).values( + [ + {"id_entity": id_entity, "id_field": id_field, **props} + for id_field, field_entities in zip(id_fields, fields_entities) + for id_entity, props in field_entities[1].items() + ] + ) + ) + + +Ajout de nouvelles erreurs de contrôle de données +""""""""""""""""""""""""""""""""""""""""""""""""" + +Il est possible que votre module nécessite de déclarer de nouveaux contrôles de données. Ces contrôles +peuvent provoquer de nouvelles erreurs que celle déclaré dans `bib_errors_type`. Il est possible d'en ajouter +comme dans l'exemple suivant : + +.. code-block:: python + + error_type = sa.Table("bib_errors_types", metadata, schema="gn_imports", autoload_with=op.get_bind()) + op.bulk_insert( + error_type, + [ + { + "error_type": "Erreur de format booléen", + "name": "INVALID_BOOL", + "description": "Le champ doit être renseigné avec une valeur binaire (0 ou 1, true ou false).", + "error_level": "ERROR", + }, + { + "error_type": "Données incohérentes d'une ou plusieurs entités", + "name": "INCOHERENT_DATA", + "description": "Les données indiquées pour une ou plusieurs entités sont incohérentes sur différentes lignes.", + "error_level": "ERROR", + }, + ... + ], + ) + +Configuration +************* + +Il faut d'abord créer une classe héritant de la classe `ImportActions` + +.. code-block:: python + + class VotreModuleImportActions(ImportActions): + def statistics_labels() -> typing.List[ImportStatisticsLabels]: + # Retourne un objet contenant les labels pour les statistiques + + def preprocess_transient_data(imprt: TImports, df) -> set: + # Effectue un pré-traitement des données dans un dataframe + + def check_transient_data(task, logger, imprt: TImports) -> None: + # Effectue la validation des données + + def import_data_to_destination(imprt: TImports) -> None: + # Importe les données dans la table de destination + + def remove_data_from_destination(imprt: TImports) -> None: + # Supprime les données de la table de destination + + def report_plot(imprt: TImports) -> StandaloneEmbedJson: + # Retourne des graphiques sur l'import + + def compute_bounding_box(imprt: TImports) -> None: + # Calcule la bounding box + +Dans cette classe on retrouve toutes les fonctions obligatoires, à implementer pour pouvoir implementer l'import dans un module. + +Méthodes à implémenter +""""""""""""""""""""" + +``statistics_labels()`` + +Fonction qui renvoie un objet de la forme suivante : + +.. code-block:: python + + {"key": "station_count", "value": "Nombre de stations importées"}, + {"key": "habitat_count", "value": "Nombre d’habitats importés"}, + + +Les statistiques sont calculées en amont, et ajoutés à l'objet import dans la section statistique. +Les valeurs des clés permettent de définir les labels à afficher pour les statistique affichées dans la liste d'imports. + +``preprocess_transient_data(imprt: TImports, df)`` + +Fonction qui permet de faire un pré-traitement sur les données de l'import, elle retourne un dataframe panda. + +``check_transient_data(task, logger, imprt: TImports)`` + +Dans cette fonction est effectuée la validation et le traitement des données de l'import. + +La fonction ``check_dates``, par exemple, utilisée dans l'import Occhab permet de valider tous les champs de type date présents dans l'import. +Elle vérifie que le format est respecté. + +La fonction ``check_transient_data`` permet de génerer les uuid manquants dans l'import, elle permet notamment de générer un UUID commun à différentes lignes de l'import quand id_origin est le même. + +``import_data_to_destination(imprt: TImports)`` + +Cette fonction permet d'implémenter l'import des données valides dans la table de destination une fois que toutes les vérifications ont été effectuées. + +``remove_data_from_destination(imprt: TImports)`` + +Cette fonction permet de supprimer les données d'un import, lors de la suppression d'un import. +C'est notamment pour pouvoir implémenter cette fonction que la colonne ``id_import`` est préconisée dans les tables de destination. + +``report_plot(imprt: TImports)`` + +Cette fonction permet de créer les graphiques affichés dans le raport d'import. +Pour créer ces graphiques on utilise la librairie bokeh (documentation : https://docs.bokeh.org/en/latest/). Il y a des exemples de création de graphiques dans l'import Occhab. + +``compute_bounding_box(imprt: TImports)`` + +Cette fonction sert à calculer la bounding box des données importées, c'est-à-dire le plus petit polygone dans lequel sont contenues toutes les données géographique importées. +Cette bounding box est affichée dans le rapport d'import une fois toutes les données validées. diff --git a/docs/images/import/import_etapes.png b/docs/images/import/import_etapes.png new file mode 100644 index 0000000000..9dbb1b8a86 Binary files /dev/null and b/docs/images/import/import_etapes.png differ diff --git a/docs/images/import/import_modele.png b/docs/images/import/import_modele.png new file mode 100644 index 0000000000..4da18b41d1 Binary files /dev/null and b/docs/images/import/import_modele.png differ diff --git a/docs/images/import/import_steps/00_imports_list.png b/docs/images/import/import_steps/00_imports_list.png new file mode 100644 index 0000000000..9285f7dd12 Binary files /dev/null and b/docs/images/import/import_steps/00_imports_list.png differ diff --git a/docs/images/import/import_steps/01_destination_choice.png b/docs/images/import/import_steps/01_destination_choice.png new file mode 100644 index 0000000000..2f0d3e06c5 Binary files /dev/null and b/docs/images/import/import_steps/01_destination_choice.png differ diff --git a/docs/images/import/import_steps/02_upload_file.png b/docs/images/import/import_steps/02_upload_file.png new file mode 100644 index 0000000000..23ea54516d Binary files /dev/null and b/docs/images/import/import_steps/02_upload_file.png differ diff --git a/docs/images/import/import_steps/03_parameter_selection.png b/docs/images/import/import_steps/03_parameter_selection.png new file mode 100644 index 0000000000..10e80e6989 Binary files /dev/null and b/docs/images/import/import_steps/03_parameter_selection.png differ diff --git a/docs/images/import/import_steps/04_01_mapping_cols.png b/docs/images/import/import_steps/04_01_mapping_cols.png new file mode 100644 index 0000000000..fb03e5156c Binary files /dev/null and b/docs/images/import/import_steps/04_01_mapping_cols.png differ diff --git a/docs/images/import/import_steps/04_02_mapping_cols_validate.png b/docs/images/import/import_steps/04_02_mapping_cols_validate.png new file mode 100644 index 0000000000..12ea9daf30 Binary files /dev/null and b/docs/images/import/import_steps/04_02_mapping_cols_validate.png differ diff --git a/docs/images/import/import_steps/05_mapping_value.png b/docs/images/import/import_steps/05_mapping_value.png new file mode 100644 index 0000000000..c996527f57 Binary files /dev/null and b/docs/images/import/import_steps/05_mapping_value.png differ diff --git a/docs/images/import/import_steps/06_01_control_data.png b/docs/images/import/import_steps/06_01_control_data.png new file mode 100644 index 0000000000..8a776f45f7 Binary files /dev/null and b/docs/images/import/import_steps/06_01_control_data.png differ diff --git a/docs/images/import/import_steps/06_02_import_part1.png b/docs/images/import/import_steps/06_02_import_part1.png new file mode 100644 index 0000000000..573aaac1d0 Binary files /dev/null and b/docs/images/import/import_steps/06_02_import_part1.png differ diff --git a/docs/images/import/import_steps/06_03_import_part2.png b/docs/images/import/import_steps/06_03_import_part2.png new file mode 100644 index 0000000000..d76043e40a Binary files /dev/null and b/docs/images/import/import_steps/06_03_import_part2.png differ diff --git a/docs/images/import/import_steps/07_report_part1.png b/docs/images/import/import_steps/07_report_part1.png new file mode 100644 index 0000000000..0df0e58b56 Binary files /dev/null and b/docs/images/import/import_steps/07_report_part1.png differ diff --git a/docs/images/import/import_steps/07_report_part2.png b/docs/images/import/import_steps/07_report_part2.png new file mode 100644 index 0000000000..d4ce57c8db Binary files /dev/null and b/docs/images/import/import_steps/07_report_part2.png differ diff --git a/docs/import_doc.rst b/docs/import_doc.rst deleted file mode 100644 index 5981f6d37f..0000000000 --- a/docs/import_doc.rst +++ /dev/null @@ -1,311 +0,0 @@ - -Module d’import ---------------- -Ce module permet d’importer des données depuis un fichier CSV dans [GeoNature](https://github.com/PnX-SI/GeoNature). - - -Configuration du module -^^^^^^^^^^^^^^^^^^^^^^^ - -Vous pouvez modifier la configuration du module dans le fichier -`geonature_config.toml` dans le dossier `config` de GeoNature, en vous inspirant -du fichier `default_config.toml.example` et en surcouchant les paramètres que vous souhaitez -(champs affichés en interface à l'étape 1, préfixe des champs ajoutés par le module, -répertoire d'upload des fichiers, SRID, encodage, séparateurs, etc). - -Pour appliquer les modifications de la configuration du module, consultez -la [rubrique dédiée de la documentation de GeoNature](https://docs.geonature.fr/installation.html#module-config). - -Configuration avancée -""""""""""""""""""""" - -Une autre partie se fait directement dans la base de données, dans les -tables `dict_fields` et `dict_themes`, permettant de masquer, ajouter, -ou rendre obligatoire certains champs à renseigner pour l'import. Un -champs masqué sera traité comme un champs non rempli, et se verra -associer des valeurs par défaut ou une information vide. Il est -également possible de paramétrer l'ordonnancement des champs (ordre, -regroupements dans des blocs) dans l'interface du mapping de champs. A -l'instar des attributs gérés dans TaxHub, il est possible de définir -des "blocs" dans la table `gn_imports.dict_themes`, et d'y attribuer -des champs (`dict_fields`) en y définissant leur ordre d'affichage. - -Permissions du module -^^^^^^^^^^^^^^^^^^^^^ - -La gestions des permissions dans le module d'import se fait via le réglage -du CRUVED à deux niveaux : au niveau de l'objet "import" et au -niveau de l'objet "mapping". - -- Le CRUVED sur l'objet "import" permet de gérer - l'affichage des imports. Une personne ayant un R = 3 verra tous les - imports de la plateforme, un R = 2 seulement les siens et ceux de son organisme - et un R = 1 seulement les siens. -- Les jeux de données selectionnables par un utilisateur lors de la - création d'un import sont eux controlés par les permissions - sur le C de l'objet "import" (combiné au R du module "Métadonnées). -- Les mappings constituent un "objet" du module d'import disposant - de droits paramétrables pour les différents utilisateurs, - indépendamment des permissions sur les imports. Le réglage des - permissions se fait dans le module "Admin" de GeoNature ("Admin" -\> - "Permissions"). -- Certains mappings sont définis comme "public" et sont vus par tout - le monde. Seuls les administrateurs (U=3) et les propriétaires de ces - mappings peuvent les modifier. -- Par exemple, avec un C = 1, R = 2, U = 1 sur les mappings, un utilisateur pourra créer - des nouveaux mappings (des modèles d'imports), modifier ses propres - mappings et voir les mappings de son organisme ainsi que les - mappings "publics". -- Si vous modifiez un mapping sur lequel vous n'avez pas les droits, - il vous sera proposé de créer un nouveau mapping vous appartenant - avec les modifications que vous avez faites, mais sans modifier le - mapping initial. Lorsqu'on a les droits de modification sur un mapping, - il est également possible de ne pas enregistrer les modifications - faites à celui-ci pour ne pas écraser le mapping inital (le mapping - sera sauvegardé uniquement avec l’import). - -Contrôles et transformations -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| Name | Description | Step | Type | -+===============================================+====================================================================================================================================================================================================================================================================+=====================+=====================+ -| file_name_error | Le nom du fichier ne contient que des chiffres | upload | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| file_name_error | Le nom du fichier commence par un chiffre | upload | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| file_name_error | Le nom du fichier fait plus de 50 caractères | upload | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| no_data | Aucune données n'est présente dans le fichier | upload | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| extension_error | L'extenstion du fichier n'est pas .csv ou .geojson | upload | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| no_file | Aucun fichier détecté | upload | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| empty | Auncun fichier envyé | upload | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| max_size | La taille du fichier est plus grande que la taille définit en paramètre | upload | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| source-error(goodtables lib) | Erreur de lecture du fichier lié à une inconsistence de données. | csv | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| format-error(goodtables lib) | Erreur de lecture (format des données incorrect). | csv | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| encoding-error(goodtables lib) | Erreur de lecture (problème d'encodage). | csv | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| blank-header(goodtables lib) | Il existe une colonne vide dans les noms de colonnes. | csv | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| duplicate-header(goodtables lib) | Plusieurs colonnes ont le même nom | csv | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| blank-row(goodtables lib) | Une ligne doit avoir au moins une colonne non vide. | csv | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| duplicate-row(goodtables lib) | Ligne dupliquée. | csv | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| extra-value(goodtables lib) | Une ligne a plus de colonne que l'entête | csv | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| missing-value(goodtables lib) | Une ligne a moins de colonne que l'entête. | csv | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| wrong id_dataset | L'utilisateur n'as pas les droits d'importer dans ce JDD (à vérifier/implémenter ?) | load raw data to db | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| psycopg2.errors.BadCopyFileFormat | Erreur lié à un problème de séparateur | load row data to db | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| missing_value | Valeur manquante pour un champ obligatoire | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| incorrect_date | Format de date incorrect - Format attendu est YYYY-MM-DD ou DD-MM-YYYY (les heures sont acceptées sous ce format: HH:MM:SS) | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| incorrect_uuid | Format d'UUID incorrect | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| incorrect_length | Chaine de charactère trop longue par rapport au champs de destination | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| incorrect_integer | Valeur incorect (ou négative) pour un champs de type entier | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| incorrect_cd_nom | Le cd_nom fourni n'est pas présent dans le taxref de l'instance | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| incorrect_cd_hab | Le cd_hab fourni n'est pas présent dans le habref de l'instance | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| date_min > date_max | date min > date_max | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| missing_uuid | UUID manquant (si calculer les UUID n'est pas coché) | data cleaning | warning | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| duplicated uuid | L'UUID fourni est déjà présent en base (dans la table synthese) - désactivable pour les instances avec beaucoup de données: paramètre `ENABLE_SYNTHESE_UUID_CHECK` | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| unique_id_sinp missing column | Si pas de colonne UUID fournie est que "calculer les UUID" est activé, on crée une colonne et on crée des UUID dedans | data cleaning | checks and corrects | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| unique_id_sinp missing values | Si UUID manquant dans une colonne UUID fournie et que "calculer les UUID" est activé, on calcul un UUID pour les valeurs manquantes | data cleaning | checks and corrects | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| missing count_min value | Si des valeurs sont manquantes pour denombrement_min, la valeur est remplacée par le paramètre DEFAULT_COUNT_VALUE | data cleaning | checks and corrects | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| missing count_max values | Si des valeurs sont manquantes pour denombrement_min, on met denombrement_min = denombrement_max | data cleaning | checks and correct | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| missing count_min column | Si pas de colonne dénombrement_min, on cree une colonne et on met la valeur du paramètre DEFAULT_COUNT_VALUE | data cleaning | checks and corrects | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| missing count_max column | Si pas de colonne denombrement_max on cree une colonne et on met denombrement_min = denombrement_max | data cleaning | checks and corrects | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| missing altitude_min and altitude_max columns | Creation de colonne et calcul si 'calcul des altitudes' est coché | data cleaning | checks and corrects | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| missing altitude_min or altitude_max values | Les altitdes sont calculées pour les valeurs manquantes si l'option est activée | data cleaning | checks and corrects | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| altitude_min > altitude_max | altitude_min > altitude_max | data cleaning | checks and corrects | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| profondeur_min > profondeur_max | profondeur_min > profondeur_max | data cleaning | checks and corrects | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| count_min > count_max | count_min > count_max | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| entity_source_pk column missing | Si pas de colonne fournie, création d'une colonne remplie avec un serial "gn_pk" | data cleaning | checks and corrects | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| entity_source_pk duplicated | entity_source_pk value dupliqué | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| incorrect_real | Valeur incorect pour un réel | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| geometry_out_of_box | Coordonnées géographiques en dehors de la bounding-box de l'instance (paramètre: `INSTANCE_BOUNDING_BOX` - desactivable via paramètre `ENABLE_BOUNDING_BOX_CHECK` | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| geometry_out_of_projection | Coordonnées géographiques en dehors du système de projection fourni | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| multiple_code_attachment | Plusieurs code de rattachement fourni pour une seule colonne (Ex code_commune = 05005, 05003) | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| multiple_attachment_type_code | Plusieurs code de rattachement fourni pour une seule ligne (code commune + code maille par ex) | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| code rattachement invalid | Le code de rattachement (code maille/département/commune) n'est pas dans le référentiel géographiques de GeoNature | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| Erreur de nomenclature | Code nomenclature erroné ; La valeur du champ n’est pas dans la liste des codes attendus pour ce champ. | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| Erreur de géometrie | Géométrie invalide ; la valeur de la géométrie ne correspond pas au format WKT. | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| Géoréférencement manquant | Géoréférencement manquant ; un géoréférencement doit être fourni, c’est à dire qu’il faut livrer : soit une géométrie, soit une ou plusieurs commune(s), ou département(s), ou maille(s) | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| Preuve numérique incorect | La preuve numérique fournie n'est pas une URL | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| Erreur champs conditionnel (désactivable) | Le champ dEEFloutage doit être remplit si le jeu de données est privé | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| Erreur champs conditionnel (désactivable) | Le champ reference_biblio doit être remplit si le statut source est 'Littérature' | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| Erreur champs preuve (désactivable) | si le champ “preuveExistante” vaut oui, alors l’un des deux champs “preuveNumérique” ou “preuveNonNumérique” doit être rempli. A l’inverse, si l’un de ces deux champs est rempli, alors “preuveExistante” ne doit pas prendre une autre valeur que “oui” (code 1) | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| Erreur de rattachement(1) - désactivable | Vérifie que si type_info_geo = 1 (Géoréférencement) alors aucun rattachement n'est fourni | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ -| Erreur de rattachement(2) - désactivable | Si une entitié de rattachement est fourni alors le type_info_geo ne doit pas être null | data cleaning | error | -+-----------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------+---------------------+ - - - -Utilisation du module d'imports -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Note : le processus a un petit peu évoluer en v2 avec notamment une -étape supplémentaire. - -Le module permet de traiter un fichier CSV -(GeoJSON non disponible dans la v2 pour le moment) sous toute -structure de données, d'établir les correspondances nécessaires entre -le format source et le format de la synthèse, et de traduire le -vocabulaire source vers les nomenclatures SINP. Il stocke et archive les -données sources et intègre les données transformées dans la synthèse de -GeoNature. Il semble préférable de prévoir un serveur disposant à minima -de 4 Go de RAM. - -1. Une fois connecté à GeoNature, accédez au module Imports. L'accueil - du module affiche une liste des imports en cours ou terminés, selon - les permissions de l'utilisateur connecté. Vous pouvez alors finir un - import en cours, ou bien commencer un nouvel import. - - .. image:: https://geonature.fr/docs/img/import/gn_imports-01.jpg - -2. Choisissez à quel JDD les données importées vont être associées. Si - vous souhaitez les associer à un nouveau JDD, il faut l'avoir créé - au préalable dans le module Métadonnées. - - .. image:: https://geonature.fr/docs/img/import/gn_imports-02.jpg - -3. Chargez le fichier CSV (GeoJSON non disponible dans la v2 pour le moment) à importer. - - .. image:: https://geonature.fr/docs/img/import/gn_imports-03.jpg - -4. Mapping des champs. Il s'agit de faire correspondre les champs du - fichier importé aux champs de la Synthèse (basé sur le standard - "Occurrences de taxons" du SINP). Vous pouvez utiliser un mapping - déjà existant ou en créer un nouveau. Le module contient par défaut - un mapping correspondant à un fichier exporté au format par défaut - de la synthèse de GeoNature. Si vous créez un nouveau mapping, il - sera ensuite réutilisable pour les imports suivants. Il est aussi - possible de choisir si les UUID uniques doivent être générés et si - les altitudes doivent être calculées automatiquement si elles ne - sont pas renseignées dans le fichier importé. - - .. image:: https://geonature.fr/docs/img/import/gn_imports-04.jpg - -6. Une fois le mapping des champs réalisé, au moins sur les champs - obligatoires, il faut alors valider le mapping pour lancer le - contrôle des données. Vous pouvez ensuite consulter les éventuelles - erreurs. Il est alors possible de corriger les données en erreurs - directement dans la base de données, dans la table temporaire des - données en cours d'import, puis de revalider le mapping, ou de - passer à l'étape suivante. Les données en erreur ne seront pas - importées et seront téléchargeables dans un fichier dédié à l'issue - du processus. - - .. image:: https://geonature.fr/docs/img/import/gn_imports-05.jpg - -7. Mapping des contenus. Il s'agit de faire correspondre les valeurs - des champs du fichier importé avec les valeurs disponibles dans les - champs de la Synthèse de GeoNature (basés par défaut sur les - nomenclatures du SINP). Par défaut les correspondances avec les - nomenclatures du SINP sous forme de code ou de libellés sont - fournies. - - .. image:: https://geonature.fr/docs/img/import/gn_imports-06.jpg - -8. La dernière étape permet d'avoir un aperçu des données à importer - et leur nombre, avant de valider l'import final dans la Synthèse de - GeoNature. - - .. image:: https://geonature.fr/docs/img/import/gn_imports-07.jpg - -Pour chaque fichier importé, les données brutes sont importées -intialement et stockées en binaire dans le champs `t_imports.source_file`. -Elles sont aussi stockées -dans une table intermédiaire, enrichie au fur et à mesure des étapes de -l'import. - -Liste des contrôles réalisés sur le fichier importé et ses données : - - -Schéma (initial et théorique) des étapes de fonctionnement du module : - -.. image:: https://geonature.fr/docs/img/import/gn_imports_etapes.png - -Modèle de données du schéma `gn_imports` du module (à adapter à la version 2.0.0) : - -.. image:: https://geonature.fr/docs/img/import/gn_imports_MCD-2020-03.png - - -Fonctionnement du module (serveur et BDD) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- J'ai un R d'au moins 1 sur le module Import : J'accède au module et je vois les imports en fonction de mon R. -- J'ai un C d'au moins 1 sur le module Import, je peux créer un import, ou terminer un import auquel j'ai accès. -- J'ai au moins un JDD actif associé au module Import. -- Je créé un nouvel Import. Le C sur le module Import permet de lister mes JDD actifs et associés au module Import, ceux de mon organisme ou tous les JDD actifs associés au module Import. -- Je choisis le JDD auquel je veux associer les données à importer. -- **Etape 1** : J'uploade mon fichier CSV (GeoJSON n'est plus disponible dans la v2 pour le moment). Le contenu du CSV est stocké en binaire dans la table des imports (`gn_imports.t_imports.source_file`). Cela permet d'analyser le fichier (encodage, séparateur...) et de télécharger les données sources. -- **Etape 2** : L'encodage, le format et le séparateur du fichier sont auto-détectés. Je peux les modifier si je le souhaite. Je renseigne le SRID parmi les SRID disponibles dans la configuration du module. -- **Etape 3** : Je choisis un modèle d'Import existant et/ou je mets en correspondance les champs du fichier source avec ceux de la Synthèse de GeoNature. Les modèles d'import listés dépendent des permissions sur l'objet "MAPPING". -La première ligne du fichier binaire est lue pour lister les champs du fichier source. -- Si je choisis un modèle et que je mappe un nouveau champs, ou une valeur différente pour un champs, je peux modifier le modèle existant, en créer un nouveau ou ne sauvegarder ces modifications dans aucun modèle. -- Si j'ai mappé une valeur source différente sur un champs déjà présent dans le modèle, il est écrasé par la nouvelle valeur si je mets à jour le modèle. Actuellement un champs de destination ne peut avoir qu'un seul champs source. Par contre un champs source peut avoir plusieurs champs de destination (`date` → `date_min` et `date` → `date_max`, par exemple). -- Les correspondances des champs sont stockées dans tous les cas en json dans le champs `gn_imports.t_imports.fieldmapping`. Cela permet de pouvoir reprendre les correspondances d'un import, même si le modèle a été modifié entre temps. -- Quand on valide l'étape 3, les données sources des champs mappés sont chargées dans la table d'import temporaire (`gn_imports.t_imports_synthese`) avec une colonne pour la valeur de la source et une pour la valeur de destination. Cela permet à l'application de faire des traitements de transformation et de contrôle sur les données. Les données sources dans des champs non mappées sont importées dans un champs json de cette table (`extra_fields`) -- **Etape 4** : Les valeurs des champs à nomenclature sont déterminées à partir du contenu de la table `gn_imports.t_imports_synthese`. Une nomenclature de destination peut avoir plusieurs valeurs source. Pour chaque type de nomenclature on liste les valeurs trouvées dans le fichier source et on propose de les associer aux valeurs des nomenclatures présentes dans GeoNature. Si le fichier source comprend des lignes vides, on propose en plus de mapper le cas "Pas de valeur". -La gestion des mappings est similaire à l'étape 3 (ils sont stockées cette fois-ci dans le champs `gn_imports.t_imports.contentmapping`). -- **Etape 5** : Il est proposé à l'utilisateur de lancer les contrôles. Ceux-ci sont exécutés en asynchrone dans tous les cas, et une barre de progression est affichée à l'utilisateur. Quand les contrôles sont terminés, le nombre d'erreurs est affiché, ainsi qu'une carte de l'étendue géographique des données et un tableau d'aperçu des données telles qu'elles seront importées. -Si il y a des erreurs, l'utilisateur peut télécharger le fichier des données sources invalides. Elles sont récupérées dans la table `gn_imports.t_imports.source_file` en ne prenant que les lignes qui ont une erreur, en se basant sur les données qui ont le champs `valid=false` dans `gn_imports.t_imports_synthese` -L'utilisateur peut alors lancer l'import des données dans la Synthèse. -Il est lancée en asynchrone dans tous les cas, et un spinner de chargement est affiché tant que l'import est en cours. Si d'autres imports sont en cours, le mécanisme asynchrone gère un système de queue pour les faire les uns après les autres et ne pas saturer le serveur. -- Il est possible de reprendre et modifier un import que celui-ci soit terminé ou non. Il est notamment possible d'uploader un nouveau fichier pour un import existant. En cas de modification d’un import existant, les données sont immédiatement supprimées de la synthèse. Les nouvelles données seront insérées lors de la nouvelle finalisation de l’import. -- Une fois les données importées, les données sont supprimées de la table temporaire (`gn_imports.t_imports_synthese`) -- **Administration des modèles** : Depuis le module ADMIN de GeoNature, il est possible de lister, afficher et modifier les modèles d'import. - - -Financement de la version 1.0.0 : DREAL et Conseil Régional -Auvergne-Rhône-Alpes. - -Financement de la version 2.0.0 : Ministère de la Transition écologique -et UMS Patrinat. - diff --git a/docs/installation.rst b/docs/installation.rst index db12ce7ffb..2424058582 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,4 +1,4 @@ -INSTALLATION +Installation ============ GeoNature repose sur les composants suivants : @@ -282,7 +282,7 @@ La mise à jour doit être réalisée avec votre utilisateur linux courant (``ge * Télécharger la dernière version de GeoNature : - :: +.. code-block:: bash wget https://github.com/PnX-SI/GeoNature/archive/X.Y.Z.zip unzip X.Y.Z.zip @@ -290,7 +290,7 @@ La mise à jour doit être réalisée avec votre utilisateur linux courant (``ge * Renommer l'ancien repertoire de l'application, ainsi que le nouveau : - :: +.. code-block:: bash mv ~/geonature/ ~/geonature_old/ mv ~/GeoNature-X.Y.Z ~/geonature/ @@ -304,18 +304,19 @@ Sauf mentions contraires dans les notes de version, vous pouvez sauter des versi * Lancez le script de ``migration.sh`` à la racine du dossier ``geonature``: -:: - ./install/migration/migration.sh 2>&1 | tee install/migration/migration.log +.. code-block:: bash + + ./install/migration/migration.sh 2>&1 | tee install/migration/migration.log Depuis la version 2.12, le script `migration.sh` peut prendre en argument le chemin vers l'ancien dossier d'installation de GeoNature. Il peut s’agir du même dossier que la nouvelle installation de GeoNature. Cela permet d'utiliser ce script si la nouvelle version de GeoNature est dans le même dossier et donc de gérer le cas où GeoNature est installé et mis à jour avec git. Dans ce cas, les commandes à exécuter sont : -:: - - cd ~/GeoNature - git status # optionnel, pour connaitre l'état et la version ou branche actuellement utilisée - git fetch - git checkout X.Y.Z # où X.Y.Z est le numéro du tag de la version vers laquelle faire la mise à jour, ou le nom de la branche à utiliser - git submodule update # ou "git config submodule.recurse true" lancé une seule fois, pour que la mise à jour des sous-modules soit relancée automatiquement à chaque "git pull" - ./install/migration/migration.sh . +.. code-block:: bash + + cd ~/GeoNature + git status # optionnel, pour connaitre l'état et la version ou branche actuellement utilisée + git fetch + git checkout X.Y.Z # où X.Y.Z est le numéro du tag de la version vers laquelle faire la mise à jour, ou le nom de la branche à utiliser + git submodule update # ou "git config submodule.recurse true" lancé une seule fois, pour que la mise à jour des sous-modules soit relancée automatiquement à chaque "git pull" + ./install/migration/migration.sh . diff --git a/docs/requirements.in b/docs/requirements.in index 4768510911..ba2fc4bfca 100644 --- a/docs/requirements.in +++ b/docs/requirements.in @@ -1,4 +1,4 @@ sphinx -sphinx_rtd_theme +sphinx-book-theme myst-parser sphinx-autoapi \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 24aef6bf85..566459a632 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,12 +4,18 @@ # # pip-compile requirements.in # +accessible-pygments==0.0.5 + # via pydata-sphinx-theme alabaster==0.7.16 # via sphinx astroid==3.3.5 # via sphinx-autoapi babel==2.16.0 - # via sphinx + # via + # pydata-sphinx-theme + # sphinx +beautifulsoup4==4.12.3 + # via pydata-sphinx-theme certifi==2024.8.30 # via requests charset-normalizer==3.4.0 @@ -17,8 +23,8 @@ charset-normalizer==3.4.0 docutils==0.21.2 # via # myst-parser + # pydata-sphinx-theme # sphinx - # sphinx-rtd-theme idna==3.10 # via requests imagesize==1.4.1 @@ -44,8 +50,13 @@ myst-parser==3.0.1 # via -r requirements.in packaging==24.1 # via sphinx +pydata-sphinx-theme==0.16.0 + # via sphinx-book-theme pygments==2.18.0 - # via sphinx + # via + # accessible-pygments + # pydata-sphinx-theme + # sphinx pyyaml==6.0.2 # via # myst-parser @@ -54,16 +65,18 @@ requests==2.32.3 # via sphinx snowballstemmer==2.2.0 # via sphinx +soupsieve==2.6 + # via beautifulsoup4 sphinx==7.4.7 # via # -r requirements.in # myst-parser + # pydata-sphinx-theme # sphinx-autoapi - # sphinx-rtd-theme - # sphinxcontrib-jquery + # sphinx-book-theme sphinx-autoapi==3.3.3 # via -r requirements.in -sphinx-rtd-theme==3.0.1 +sphinx-book-theme==1.1.3 # via -r requirements.in sphinxcontrib-applehelp==2.0.0 # via sphinx @@ -71,8 +84,6 @@ sphinxcontrib-devhelp==2.0.0 # via sphinx sphinxcontrib-htmlhelp==2.1.0 # via sphinx -sphinxcontrib-jquery==4.1 - # via sphinx-rtd-theme sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==2.0.0 @@ -84,7 +95,9 @@ stdlib-list==0.11.0 tomli==2.0.2 # via sphinx typing-extensions==4.12.2 - # via astroid + # via + # astroid + # pydata-sphinx-theme urllib3==2.2.3 # via requests zipp==3.20.2 diff --git a/docs/user-manual.rst b/docs/user-manual.rst index 5d4bcd6f58..e6fab91972 100644 --- a/docs/user-manual.rst +++ b/docs/user-manual.rst @@ -1,4 +1,4 @@ -MANUEL UTILISATEUR +Manuel utilisateur ================== Authentification @@ -593,3 +593,6 @@ Les permissions dont disposent le groupe ou l'utilisateur sont indiquées en ver Pour les utilisateurs sont listées les permissions qui lui sont attribués directement individuellement, mais aussi les permissions effectives qui s'appliquent à lui (selon les groupes auquel il appartient) : .. image :: https://github.com/PnX-SI/GeoNature/assets/4418840/75486b5c-a571-4c3a-9fd5-ff57328776c7 + + +.. include:: utilisateur/import.rst \ No newline at end of file diff --git a/docs/utilisateur/import.rst b/docs/utilisateur/import.rst new file mode 100644 index 0000000000..067fe9255e --- /dev/null +++ b/docs/utilisateur/import.rst @@ -0,0 +1,121 @@ +.. raw:: html + + + +.. role:: red + +Import +------ + +Depuis sa version 2.15, le module Import a été intégré à GeoNature et il permet d'importer des données depuis un fichier CSV dans deux modules : + +- Synthèse +- Occhab + +Créer un import +""""""""""""""" + +Pour réaliser un import dans une des destinations de GeoNature, vous devez : + +1. Une fois connecté à GeoNature, accédez au module Import. L'accueil + du module affiche la "liste des imports" en cours ou terminés, selon + les permissions de l'utilisateur connecté. Vous pouvez alors finir un + import en cours, ou bien commencer un nouvel import. + +.. image:: images/import/import_steps/00_imports_list.png + +2. Pour commencer un nouvel import, cliquez sur le bouton "+" en bas de la liste des imports. Dans la fenêtre qui s'affiche, il vous est demandé de sélectionner la destination de votre import. + +.. image:: images/import/import_steps/01_destination_choice.png + +3. Une fois la destination choisie, vous serez redirigé vers une nouvelle page. + Dans ce nouveau formulaire, choisissez le jeu de données qui sera associé aux données importées. + Puis, téléverser le fichier CSV contenant les données que vous souhaitez importer. Une fois les + champs remplis, cliquez sur le bouton "Suivant". + +.. image:: images/import/import_steps/02_upload_file.png + +4. Dans ce nouveau formulaire, indiquez les paramètres de lecture de votre fichier. Plusieurs +paramètres seront automatiquement détectés par GeoNature. Une fois, les champs remplis, cliquez sur le bouton "Suivant". + +.. image:: images/import/import_steps/03_parameter_selection.png + +5. Maintenant que le fichier est téleversé et chargé, il s'agit de faire + correspondre les champs du fichier importé aux champs du modèle des entités (e.g. Station). + Pour vous aider dans la saisie, vous pouvez utiliser un mapping + déjà existant ou en créer un nouveau pour l'utiliser dans de futurs imports. + Le module contient par défaut un mapping correspondant à un fichier exporté au format par défaut + des modules Synthèse et Occhab. Si vous créez un nouveau mapping, il + sera réutilisable pour les imports suivants. Une fois, la mise en correspondance terminée, + cliquez sur le bouton "Suivant". + +.. image:: images/import/import_steps/04_01_mapping_cols.png + + **Notes.** A la fin du formulaire, vous pouvez visualiser le nombre de correspondances effectuées + et les colonnes du fichier source qui n'ont pas été utilisées. + +.. image:: images/import/import_steps/04_02_mapping_cols_validate.png + +6. La correspondance entre le fichier source et le modèle effectué, il faut maintenant faire + correspondre des valeurs des colonnes contenant des données de nomenclatures (e.g. Etat Biologique de l'observation) + avec les nomenclatures disponibles dans la base. Une fois la correspondance terminée, + cliquez sur le bouton "Suivant". + +.. image:: images/import/import_steps/05_mapping_value.png + + +7. Pour pouvoir importer les données présentes dans le fichier source, il est nécessaire d' +effectuer des contrôles sur les données : vérification des types, vérification des formats de données (dates), +vérification de cohérence de données (date début < date fin), etc. Pour lancer, le contrôle de données cliquez +sur le bouton "Lancer la vérification". + +.. image:: images/import/import_steps/06_01_control_data.png + +8. Une fois la vérification des données effectuée, un aperçu des données valides ainsi que leur emprise spatiale (*bounding box*) sont affichés. Si des erreurs sont présentes dans les données, un bouton "Rapport d'import/erreurs/avertissement" permet d'afficher les erreurs et d'ajuster votre fichier source ou les paramètres de l'import. Si l'aperçu des données qui seront importées vous convient, cliquez sur le bouton "Importer les n entités valides". + +.. image:: images/import/import_steps/06_02_import_part1.png + +9. Une fois l'import terminé, un rapport récapitulatif est affiché avec les différents paramètres +de l'import mais aussi plusieurs indicateurs statistiques sous forme de tableau et de graphique(s). +Il est aussi possible d'exporter une version PDF de ce rapport. + +.. image:: images/import/import_steps/07_report_part1.png + +Modifier un import +"""""""""""""""""" + +Pour modifier un import, rendez-vous dans la "Liste des imports", cliquez sur l'icone en forme de "crayon" dans la colonne "Actions". + +:red:`!! Attention !! La modification d'un import terminé provoquera la suppression des +données importées dans la table temporaire et dans la destination.` + +Supprimer un import +""""""""""""""""""" + +Pour supprimer un import, il suffit de cliquer sur l'icone en forme de poubelle dans la colonne "Actions". + +:red:`!! Attention !! La suppression d'un import terminé entrainera la suppression des données dans la destination.` + +Exemple de fichier CSV pour l'import Occhab +""""""""""""""""""""""""""""""""""""""""""" + +Ci-dessous un exemple de fichier CSV avec les colonnes et le contenu attendu dans l'import de données vers Occhab. + +L'import se fait dans un fichier tableur à plat mais permet d'importer des stations comprenant plusieurs habitats chacunes, mais aussi d'importer des habitats à associer à des stations existantes dans la BDD. + ++------------+--------------+------------------+----------+--------+-----------------------------------------------------------------------------------------------------------------------+ +| id_origine | UUID_station | geometry_station | UUID_hab | cd_hab | STATUTS | ++============+==============+==================+==========+========+=======================================================================================================================+ +| 5 | | POINT (30 10) | | 27 | Ajout d’une station à laquelle on associe un habitat (leurs UUIDs seront générés) | +| 5 | | POINT (30 10) | | 32 | Ajout d’un second habitat dans la station précédemment créée (l’UUID habitat sera généré) | +| | AAA | POINT (15 10) | | 18 | Ajout d’une station à l'aquelle on associe un habitat (génération de l’UUID de l’habitat) | +| | CCC | POINT (9 5) | | 11 | Ajout d’une station à laquelle on associe un habitat (génération de l’UUID de l’habitat uniquement) | +| | CCC | | | 15 | Ajout d’un habitat dans cette station (répéter les informations d’une station déclarée dans le fichier est optionnel) | +| | XXX | | | 22 | Ajout d’un habitat dans une station existante dans la BDD (identifié par l’UUID XXX) | +| 6 | | POINT (9 4) | | | Ajout d’une station uniquement | +| 6 | | POINT (9 4) | | | Ligne ignorée car doublon de la ligne 8 | +| | BBB | POINT (9 4) | | 55 | Provoque une erreur car il y a une incohérence dans les données d’une station sur différentes lignes | +| | BBB | POINT (20 3) | | 58 | Provoque une erreur car il y a une incohérence dans les données d’une station sur différentes lignes | ++------------+--------------+------------------+----------+--------+-----------------------------------------------------------------------------------------------------------------------+ + +Plus d'exemples sont disponibles dans le fichier ``valid_file.csv`` dans le dossier ``backend/geonature/tests/imports/files/occhab/valid_file.csv``. diff --git a/frontend/cypress/e2e/occhab-spec.js b/frontend/cypress/e2e/occhab-spec.js index 0dd368585b..043db50d6a 100644 --- a/frontend/cypress/e2e/occhab-spec.js +++ b/frontend/cypress/e2e/occhab-spec.js @@ -17,6 +17,7 @@ describe('Testing occhab', () => { cy.get( '[data-qa="pnx-occhab-form"] > div:nth-child(1) > pnx-map > div > div.leaflet-container.leaflet-touch.leaflet-fade-anim.leaflet-grab.leaflet-touch-drag.leaflet-touch-zoom > div.leaflet-control-container > div.leaflet-top.leaflet-left > div.leaflet-draw.leaflet-control > div:nth-child(1) > div > a' ).click(); + cy.wait(10000); const positions = [ [250, 250], [300, 250], @@ -26,7 +27,7 @@ describe('Testing occhab', () => { ]; positions.forEach((pos) => { cy.get(canvas).click(pos[0], pos[1]); - cy.wait(1000); + cy.wait(10000); }); cy.get('#validateButton').should('be.disabled'); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fad16e5a17..256e131d93 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -45,7 +45,7 @@ "leaflet": "~1.9.4", "leaflet-draw": "^1.0.4", "leaflet-image": "^0.4.0", - "leaflet.locatecontrol": "^0.82.0", + "leaflet.locatecontrol": "^0.79.0", "leaflet.markercluster": "^1.5.3", "lodash": "^4.17.21", "material-design-icons": "^3.0.1", @@ -11238,9 +11238,9 @@ } }, "node_modules/leaflet.locatecontrol": { - "version": "0.82.0", - "resolved": "https://registry.npmjs.org/leaflet.locatecontrol/-/leaflet.locatecontrol-0.82.0.tgz", - "integrity": "sha512-+lvtZ7tfqgWoUvTYRqDggw50HUZJGvaSgSU5JYvl5H4WtlnHek2R0TsL0EqROhT3igPFwYVV87bFT/ps1SqyGA==", + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/leaflet.locatecontrol/-/leaflet.locatecontrol-0.79.0.tgz", + "integrity": "sha512-h64QIHFkypYdr90lkSfjKvPvvk8/b8UnP3m9WuoWdp5p2AaCWC0T1NVwyuj4rd5U4fBW3tQt4ppmZ2LceHMIDg==", "license": "MIT" }, "node_modules/leaflet.markercluster": { diff --git a/frontend/package.json b/frontend/package.json index ba3e4df808..829c0db1f1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -50,7 +50,7 @@ "leaflet": "~1.9.4", "leaflet-draw": "^1.0.4", "leaflet-image": "^0.4.0", - "leaflet.locatecontrol": "^0.82.0", + "leaflet.locatecontrol": "^0.79.0", "leaflet.markercluster": "^1.5.3", "lodash": "^4.17.21", "material-design-icons": "^3.0.1", diff --git a/frontend/src/app/GN2CommonModule/form/datalist/datalist.component.ts b/frontend/src/app/GN2CommonModule/form/datalist/datalist.component.ts index 317a8e2cb3..0a2c830a29 100644 --- a/frontend/src/app/GN2CommonModule/form/datalist/datalist.component.ts +++ b/frontend/src/app/GN2CommonModule/form/datalist/datalist.component.ts @@ -1,13 +1,4 @@ -import { filter } from 'rxjs/operators'; -import { - Component, - OnInit, - Input, - OnChanges, - DoCheck, - IterableDiffers, - IterableDiffer, -} from '@angular/core'; +import { Component, OnInit, Input } from '@angular/core'; import { DataFormService } from '../data-form.service'; import { GenericFormComponent } from '@geonature_common/form/genericForm.component'; import { CommonService } from '../../service/common.service'; @@ -37,6 +28,7 @@ export class DatalistComponent extends GenericFormComponent implements OnInit { @Input() filters = {}; // help @Input() default; + @Input() nullDefault; @Input() dataPath: string; // pour atteindre la liste si elle n'est pas à la racine de la réponse de l'api. // si on a 'data/liste' on mettra dataPath='data' @@ -70,7 +62,15 @@ export class DatalistComponent extends GenericFormComponent implements OnInit { getFilteredValues() { let values = this.values || []; - + // if(this.nullDefault){ + // values.push() + // } + if (this.nullDefault && !this.required) { + let obj = {}; + obj[this.keyValue] = null; + obj[this.keyLabel] = '-- Aucun --'; + values.unshift(obj); + } values = values // filter search .filter( @@ -133,20 +133,34 @@ export class DatalistComponent extends GenericFormComponent implements OnInit { this.filteredValues.length === 1 && !(this.parentFormControl.value && this.parentFormControl.value.length) ) { - const val = this.values[0][this.keyValue]; - this.parentFormControl.patchValue(this.multiple ? [val] : val); + const val = this.nullDefault ? null : this.values[0][this.keyValue]; + this.parentFormControl.patchValue(this.multiple && !this.nullDefault ? [val] : val); } + // valeur par défaut (depuis input value) - if (!this.parentFormControl.value && this.default) { + if ( + (!this.parentFormControl.value || + (Array.isArray(this.parentFormControl.value) && + this.parentFormControl.value.length == 0)) && + this.default + ) { const value = this.multiple ? this.default : [this.default]; - const res = value.map((val) => - typeof val === 'object' - ? (this.filteredValues.find((v) => - Object.keys(val).every((key) => v[key] === val[key]) - ) || {})[this.keyValue] - : val - ); - this.parentFormControl.patchValue(this.multiple ? res : res[0]); + // check if the default value is in the provided values + const valuesID = this.values.map((el) => el[this.keyValue]); + const defaultValuesID = value.map((el) => el[this.keyValue]); + const defaultValueIsInValues = valuesID.some((el) => defaultValuesID.includes(el)); + + // patch value only if default value is in values + if (defaultValueIsInValues) { + const res = value.map((val) => + typeof val === 'object' + ? (this.filteredValues.find((v) => + Object.keys(val).every((key) => v[key] === val[key]) + ) || {})[this.keyValue] + : val + ); + this.parentFormControl.patchValue(this.multiple ? res : res[0]); + } } this.parentFormControl.markAsTouched(); } diff --git a/frontend/src/app/GN2CommonModule/form/dynamic-form/dynamic-form.component.html b/frontend/src/app/GN2CommonModule/form/dynamic-form/dynamic-form.component.html index 04200a1793..9d96cd8045 100644 --- a/frontend/src/app/GN2CommonModule/form/dynamic-form/dynamic-form.component.html +++ b/frontend/src/app/GN2CommonModule/form/dynamic-form/dynamic-form.component.html @@ -316,8 +316,15 @@ [idMenu]="formDefComp['id_menu']" [idList]="formDefComp['id_list']" [codeList]="formDefComp['code_list']" + [multiSelect]="formDefComp['multi_select']" > + + {{ formDefComp['help'] }} diff --git a/frontend/src/app/GN2CommonModule/form/media/display-medias.component.html b/frontend/src/app/GN2CommonModule/form/media/display-medias.component.html index 2d70eefe8e..1a3f6fdc28 100644 --- a/frontend/src/app/GN2CommonModule/form/media/display-medias.component.html +++ b/frontend/src/app/GN2CommonModule/form/media/display-medias.component.html @@ -30,12 +30,11 @@
- + > Liste des imports > {{ 'Import.ChooseDestination' | translate }} data-qa="import-new-modal-destinations" > @@ -52,7 +52,7 @@