From 67f7bdea3dcdc251c4beb3d45dbeb8b105f42d24 Mon Sep 17 00:00:00 2001 From: jacquesfize Date: Wed, 12 Aug 2026 12:00:44 +0200 Subject: [PATCH 1/4] chore : add SQLA 2 compatibility --- .github/workflows/pytest.yml | 2 +- backend/gn_module_monitoring/blueprint.py | 2 +- .../command/imports/protocol.py | 2 +- .../gn_module_monitoring/monitoring/admin.py | 4 +- .../gn_module_monitoring/monitoring/models.py | 81 +++++++++++-------- .../monitoring/repositories.py | 10 ++- 6 files changed, 57 insertions(+), 44 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index dd04a3da3..33b51afc8 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -23,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - geonature_ref: ["master"] # Mettre version compatible + geonature_ref: ["sqla2"] # Mettre version compatible uses: pnx-si/geonature/.github/workflows/gn-module-pytest.yml@develop with: geonature_ref: ${{ matrix.geonature_ref }} diff --git a/backend/gn_module_monitoring/blueprint.py b/backend/gn_module_monitoring/blueprint.py index 59c723f60..cba6fe478 100644 --- a/backend/gn_module_monitoring/blueprint.py +++ b/backend/gn_module_monitoring/blueprint.py @@ -79,4 +79,4 @@ def set_current_module(): for cmd in commands: blueprint.cli.add_command(cmd) -flask_admin.add_view(BibTypeSiteView(DB.session, name="Types de site", category="Monitorings")) +flask_admin.add_view(BibTypeSiteView(DB, name="Types de site", category="Monitorings")) diff --git a/backend/gn_module_monitoring/command/imports/protocol.py b/backend/gn_module_monitoring/command/imports/protocol.py index 09148d428..0b87347cf 100644 --- a/backend/gn_module_monitoring/command/imports/protocol.py +++ b/backend/gn_module_monitoring/command/imports/protocol.py @@ -330,7 +330,7 @@ def update_protocol(module_data, module_code, fields_to_delete, update_label_onl delete_bib_fields(fields_to_delete) table_name = f"t_imports_{module_code.lower()}" - DB.engine.execute(text(f"DROP TABLE IF EXISTS gn_imports.{table_name}")) + DB.session.execute(text(f"DROP TABLE IF EXISTS gn_imports.{table_name}")) create_sql_import_table_protocol(module_code, protocol_data) diff --git a/backend/gn_module_monitoring/monitoring/admin.py b/backend/gn_module_monitoring/monitoring/admin.py index 7c4485510..90260263c 100644 --- a/backend/gn_module_monitoring/monitoring/admin.py +++ b/backend/gn_module_monitoring/monitoring/admin.py @@ -57,9 +57,9 @@ class BibTypeSiteView(CruvedProtectedMixin, ModelView): module_code = "MONITORINGS" object_code = "TYPES_SITES" - def __init__(self, session, **kwargs): + def __init__(self, db, **kwargs): # Référence au model utilisé - super(BibTypeSiteView, self).__init__(BibTypeSite, session, **kwargs) + super(BibTypeSiteView, self).__init__(BibTypeSite, db, **kwargs) def get_only_nomenclature_asc(): return ( diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index a54e9032e..8f413ff5a 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -2,6 +2,8 @@ Modèles SQLAlchemy pour les modules de suivi """ +from typing import Any, Optional + import geoalchemy2 from flask import g @@ -9,7 +11,7 @@ from uuid import uuid4 from sqlalchemy import join, select, func, and_ -from sqlalchemy.orm import column_property, aliased +from sqlalchemy.orm import Mapped, column_property, aliased, mapped_column from sqlalchemy.dialects.postgresql import JSONB, UUID @@ -86,11 +88,15 @@ class TMonitoringObservationDetails(DB.Model, PermissionModel, ObservationDetail __tablename__ = "t_observation_details" __table_args__ = {"schema": "gn_monitoring"} - id_observation_detail = DB.Column(DB.Integer, primary_key=True, nullable=False, unique=True) + id_observation_detail: Mapped[int] = mapped_column(DB.Integer, primary_key=True, unique=True) - id_observation = DB.Column(DB.ForeignKey("gn_monitoring.t_observations.id_observation")) - data = DB.Column(JSONB) - uuid_observation_detail = DB.Column(UUID(as_uuid=True), default=uuid4) + id_observation: Mapped[Optional[int]] = mapped_column( + DB.ForeignKey("gn_monitoring.t_observations.id_observation") + ) + data: Mapped[Optional[Any]] = mapped_column(JSONB) + uuid_observation_detail: Mapped[Optional[Any]] = mapped_column( + UUID(as_uuid=True), default=uuid4 + ) medias = DB.relationship( TMedias, @@ -117,12 +123,11 @@ class TMonitoringObservations(TObservations, PermissionModel, ObservationsQuery) "polymorphic_identity": "monitoring_observation", } - data = DB.Column(JSONB) + data: Mapped[Optional[Any]] = mapped_column(JSONB) - id_observation = DB.Column( + id_observation: Mapped[int] = mapped_column( DB.ForeignKey("gn_monitoring.t_observations.id_observation"), primary_key=True, - nullable=False, ) medias = DB.relationship( @@ -188,12 +193,12 @@ class TMonitoringVisits(TBaseVisits, PermissionModel, VisitQuery): "polymorphic_identity": "monitoring_visit", } - id_base_visit = DB.Column( + id_base_visit: Mapped[int] = mapped_column( DB.ForeignKey("gn_monitoring.t_base_visits.id_base_visit"), primary_key=True, ) - data = DB.Column(JSONB) + data: Mapped[Optional[Any]] = mapped_column(JSONB) medias = DB.relationship( TMedias, @@ -286,11 +291,11 @@ class TMonitoringSites(TBaseSites, PermissionModel, SitesQuery): "polymorphic_identity": "monitoring_site", } - id_base_site = DB.Column( - DB.ForeignKey("gn_monitoring.t_base_sites.id_base_site"), nullable=False, primary_key=True + id_base_site: Mapped[int] = mapped_column( + DB.ForeignKey("gn_monitoring.t_base_sites.id_base_site"), primary_key=True ) - id_sites_group = DB.Column( + id_sites_group: Mapped[Optional[int]] = mapped_column( DB.ForeignKey( "gn_monitoring.t_sites_groups.id_sites_group", ), @@ -300,7 +305,8 @@ class TMonitoringSites(TBaseSites, PermissionModel, SitesQuery): back_populates="sites", uselist=False, ) - data = DB.Column(JSONB) + + data: Mapped[Optional[Any]] = mapped_column(JSONB) modules = DB.relationship( "TMonitoringModules", @@ -455,21 +461,25 @@ class TMonitoringSitesGroups(DB.Model, PermissionModel, SitesGroupsQuery): __tablename__ = "t_sites_groups" __table_args__ = {"schema": "gn_monitoring"} - id_sites_group = DB.Column(DB.Integer, primary_key=True, nullable=False, unique=True) - id_digitiser = DB.Column(DB.Integer, DB.ForeignKey("utilisateurs.t_roles.id_role")) + id_sites_group: Mapped[int] = mapped_column(DB.Integer, primary_key=True, unique=True) + id_digitiser: Mapped[Optional[int]] = mapped_column( + DB.Integer, DB.ForeignKey("utilisateurs.t_roles.id_role") + ) digitiser = DB.relationship( User, primaryjoin=(User.id_role == id_digitiser), foreign_keys=[id_digitiser] ) - uuid_sites_group = DB.Column(UUID(as_uuid=True), default=uuid4) + uuid_sites_group: Mapped[Optional[Any]] = mapped_column(UUID(as_uuid=True), default=uuid4) - sites_group_name = DB.Column(DB.Unicode) - sites_group_code = DB.Column(DB.Unicode) - sites_group_description = DB.Column(DB.Unicode) + sites_group_name: Mapped[Optional[str]] = mapped_column(DB.Unicode) + sites_group_code: Mapped[Optional[str]] = mapped_column(DB.Unicode) + sites_group_description: Mapped[Optional[str]] = mapped_column(DB.Unicode) - comments = DB.Column(DB.Unicode) - geom = DB.Column(geoalchemy2.types.Geometry("GEOMETRY", 4326, nullable=True)) - data = DB.Column(JSONB) + comments: Mapped[Optional[str]] = mapped_column(DB.Unicode) + geom: Mapped[Optional[Any]] = mapped_column( + geoalchemy2.types.Geometry("GEOMETRY", 4326, nullable=True) + ) + data: Mapped[Optional[Any]] = mapped_column(JSONB) medias = DB.relationship( TMedias, @@ -500,8 +510,8 @@ class TMonitoringSitesGroups(DB.Model, PermissionModel, SitesGroupsQuery): .scalar_subquery() ) - altitude_min = DB.Column(DB.Integer) - altitude_max = DB.Column(DB.Integer) + altitude_min: Mapped[Optional[int]] = mapped_column(DB.Integer) + altitude_max: Mapped[Optional[int]] = mapped_column(DB.Integer) geom_geojson = column_property( select(func.st_asgeojson(func.st_convexHull(func.st_collect(TMonitoringSites.geom)))) @@ -573,22 +583,23 @@ class TMonitoringModules(TModules, PermissionModel, MonitoringQuery): __import_actions__ = MonitoringImportActions - id_module = DB.Column( + id_module: Mapped[int] = mapped_column( DB.ForeignKey("gn_commons.t_modules.id_module"), primary_key=True, - nullable=False, unique=True, ) - uuid_module_complement = DB.Column(UUID(as_uuid=True), default=uuid4) + uuid_module_complement: Mapped[Optional[Any]] = mapped_column( + UUID(as_uuid=True), default=uuid4 + ) - id_list_observer = DB.Column(DB.Integer) - id_list_taxonomy = DB.Column(DB.Integer) - cd_nom = DB.Column(DB.Integer) + id_list_observer: Mapped[Optional[int]] = mapped_column(DB.Integer) + id_list_taxonomy: Mapped[Optional[int]] = mapped_column(DB.Integer) + cd_nom: Mapped[Optional[int]] = mapped_column(DB.Integer) - taxonomy_display_field_name = DB.Column(DB.Unicode) - b_synthese = DB.Column(DB.Boolean) - b_draw_sites_group = DB.Column(DB.Boolean) + taxonomy_display_field_name: Mapped[Optional[str]] = mapped_column(DB.Unicode) + b_synthese: Mapped[Optional[bool]] = mapped_column(DB.Boolean) + b_draw_sites_group: Mapped[Optional[bool]] = mapped_column(DB.Boolean) medias = DB.relationship( TMedias, @@ -638,7 +649,7 @@ class TMonitoringModules(TModules, PermissionModel, MonitoringQuery): "BibTypeSite", secondary=cor_module_type, ) - data = DB.Column(JSONB) + data: Mapped[Optional[Any]] = mapped_column(JSONB) visits = DB.relationship( TMonitoringVisits, diff --git a/backend/gn_module_monitoring/monitoring/repositories.py b/backend/gn_module_monitoring/monitoring/repositories.py index 7a3e0b020..6e08ad09f 100644 --- a/backend/gn_module_monitoring/monitoring/repositories.py +++ b/backend/gn_module_monitoring/monitoring/repositories.py @@ -90,7 +90,7 @@ def process_synthese(self, process_module=False, limit=1000): table_name = "v_synthese_{}".format(self._module_code.lower()) # Test de l'existance de la colonne de synchronisation sur la vue synthese - column_exist = DB.engine.execute( + column_exist = DB.session.execute( text(""" SELECT count(*) FROM information_schema.columns @@ -98,9 +98,11 @@ def process_synthese(self, process_module=False, limit=1000): AND table_name=:table_name AND column_name=:column_name; """), - table_schema="gn_monitoring", - table_name=table_name, - column_name=self.config_param("id_field_name"), + params={ + "table_schema": "gn_monitoring", + "table_name": table_name, + "column_name": self.config_param("id_field_name"), + }, ).fetchone() if column_exist[0] == 0: From 3bbee31082cf1997665519f7aa1cea40281d45df Mon Sep 17 00:00:00 2001 From: CynthiaBorotPNV Date: Mon, 3 Aug 2026 17:06:44 +0200 Subject: [PATCH 2/4] fix: Change the individuals object name, it will be manage in core --- ...47114b4d_rename_individuals_permissions.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 backend/gn_module_monitoring/migrations/bbdd47114b4d_rename_individuals_permissions.py diff --git a/backend/gn_module_monitoring/migrations/bbdd47114b4d_rename_individuals_permissions.py b/backend/gn_module_monitoring/migrations/bbdd47114b4d_rename_individuals_permissions.py new file mode 100644 index 000000000..32c0a8a33 --- /dev/null +++ b/backend/gn_module_monitoring/migrations/bbdd47114b4d_rename_individuals_permissions.py @@ -0,0 +1,42 @@ +"""rename_individuals_permissions + +Revision ID: bbdd47114b4d +Revises: 3d39820c9ab7 +Create Date: 2026-08-03 16:47:09.315441 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'bbdd47114b4d_rename_permissions' +down_revision = '3d39820c9ab7' +branch_labels = None +depends_on = 'ad8b797d89c0' + + +def upgrade(): + conn = op.get_bind() + + op.execute( + sa.text( + """ + DELETE FROM gn_permissions.t_objects + WHERE code_object = 'MONITORINGS_INDIVIDUALS' + """ + ) + ) + + +def downgrade(): + conn = op.get_bind() + + op.execute( + sa.text( + """ + INSERT INTO gn_permissions.t_objects (id_object, code_object, description_object) + VALUES(nextval('gn_permissions.t_objects_id_object_seq'::regclass), 'MONITORINGS_INDIVIDUALS', 'Permissions sur les individus'); + """ + ) + ) From f469cda9963080dfea009839e17611069a6ecdb0 Mon Sep 17 00:00:00 2001 From: CynthiaBorotPNV Date: Wed, 5 Aug 2026 10:19:52 +0200 Subject: [PATCH 3/4] fix: Change all MONITORIGINGS_INDIVIDUALS objec ref to INDIVIDUALS --- .../command/imports/constant.py | 2 +- .../command/imports/protocol.py | 1 - .../command/imports/sql.py | 6 +- backend/gn_module_monitoring/command/sql.py | 2 +- .../gn_module_monitoring/conf_schema_toml.py | 2 +- ...47114b4d_rename_individuals_permissions.py | 24 +- .../gn_module_monitoring/monitoring/models.py | 8 +- .../routes/individuals.py | 6 +- .../gn_module_monitoring/tests/conftest.py | 3 +- .../tests/fixtures/__init__.py | 3 +- .../tests/fixtures/individual.py | 2 - .../tests/fixtures/module.py | 2 +- .../tests/fixtures/monitoring_modules.py | 265 ++++++++++++++++++ .../tests/test_commands/test_commands.py | 7 +- .../tests/test_import/conftest.py | 1 - .../test_import/test_imports_monitoring.py | 10 +- conftest.py | 11 - docs/individuals.md | 2 +- 18 files changed, 299 insertions(+), 58 deletions(-) create mode 100644 backend/gn_module_monitoring/tests/fixtures/monitoring_modules.py delete mode 100644 conftest.py diff --git a/backend/gn_module_monitoring/command/imports/constant.py b/backend/gn_module_monitoring/command/imports/constant.py index 2fb0679dc..f15a2f107 100644 --- a/backend/gn_module_monitoring/command/imports/constant.py +++ b/backend/gn_module_monitoring/command/imports/constant.py @@ -85,7 +85,7 @@ "MONITORINGS_GRP_SITES": {"label": "groupes de sites", "actions": ["C", "R", "U", "D"]}, "MONITORINGS_SITES": {"label": "sites", "actions": ["C", "R", "U", "D"]}, "MONITORINGS_VISITES": {"label": "visites", "actions": ["C", "R", "U", "D"]}, - "MONITORINGS_INDIVIDUALS": {"label": "individus", "actions": ["C", "R", "U", "D"]}, + "INDIVIDUALS": {"label": "individus", "actions": ["C", "R", "U", "D"]}, "MONITORINGS_MARKINGS": {"label": "marquages", "actions": ["C", "R", "U", "D"]}, } diff --git a/backend/gn_module_monitoring/command/imports/protocol.py b/backend/gn_module_monitoring/command/imports/protocol.py index 0b87347cf..98bb7e9fa 100644 --- a/backend/gn_module_monitoring/command/imports/protocol.py +++ b/backend/gn_module_monitoring/command/imports/protocol.py @@ -294,7 +294,6 @@ def update_protocol(module_data, module_code, fields_to_delete, update_label_onl Booléen indiquant si la mise à jour a réussi. """ try: - DB.session.rollback() module_label = module_data["module"].get("module_label") destination = DB.session.execute( diff --git a/backend/gn_module_monitoring/command/imports/sql.py b/backend/gn_module_monitoring/command/imports/sql.py index e8f47f8c8..f5fd5eb10 100644 --- a/backend/gn_module_monitoring/command/imports/sql.py +++ b/backend/gn_module_monitoring/command/imports/sql.py @@ -10,6 +10,7 @@ Boolean, ForeignKey, PrimaryKeyConstraint, + text, ) from geonature.utils.env import DB @@ -35,7 +36,8 @@ def check_rows_exist_in_import_table(module_code: str) -> bool: table_name = f"t_imports_{module_code.lower()}" query = f"SELECT * FROM gn_imports.{table_name} LIMIT 1;" try: - result = DB.session.execute(query).fetchone() + with DB.session.begin_nested(): + result = DB.session.execute(text(query)).fetchone() return result is not None except Exception as e: print(f"Erreur lors de la vérification de l'existence de la table : {str(e)}") @@ -47,7 +49,7 @@ def create_sql_import_table_protocol(module_code: str, protocol_data): Create import table using SQLAlchemy metadata """ table = get_imports_table_metadata(module_code, protocol_data) - table.metadata.create_all(DB.engine) + table.metadata.create_all(DB.session.connection()) print(f"La table transitoire d'importation pour {module_code} a été créée.") diff --git a/backend/gn_module_monitoring/command/sql.py b/backend/gn_module_monitoring/command/sql.py index 16b6ac3b9..e6860cc11 100644 --- a/backend/gn_module_monitoring/command/sql.py +++ b/backend/gn_module_monitoring/command/sql.py @@ -65,7 +65,7 @@ def execute_sql_file(dir, file, module_code, forbidden_instruction=[]): with DB.engine.begin() as conn: conn.execute( text(sql_content), - module_code=module_code, + {"module_code": module_code}, ) except Exception as e: raise Exception("{} - erreur dans le script {} : {}".format(module_code, file, e)) diff --git a/backend/gn_module_monitoring/conf_schema_toml.py b/backend/gn_module_monitoring/conf_schema_toml.py index a31abc63e..c56e90b41 100644 --- a/backend/gn_module_monitoring/conf_schema_toml.py +++ b/backend/gn_module_monitoring/conf_schema_toml.py @@ -14,7 +14,7 @@ "visit": "MONITORINGS_VISITES", "observation": "MONITORINGS_VISITES", "observation_detail": "MONITORINGS_VISITES", - "individual": "MONITORINGS_INDIVIDUALS", + "individual": "INDIVIDUALS", "marking": "MONITORINGS_MARKINGS", } diff --git a/backend/gn_module_monitoring/migrations/bbdd47114b4d_rename_individuals_permissions.py b/backend/gn_module_monitoring/migrations/bbdd47114b4d_rename_individuals_permissions.py index 32c0a8a33..f7839f760 100644 --- a/backend/gn_module_monitoring/migrations/bbdd47114b4d_rename_individuals_permissions.py +++ b/backend/gn_module_monitoring/migrations/bbdd47114b4d_rename_individuals_permissions.py @@ -5,38 +5,30 @@ Create Date: 2026-08-03 16:47:09.315441 """ + from alembic import op import sqlalchemy as sa - # revision identifiers, used by Alembic. -revision = 'bbdd47114b4d_rename_permissions' -down_revision = '3d39820c9ab7' +revision = "bbdd47114b4d_rename_permissions" +down_revision = "3d39820c9ab7" branch_labels = None -depends_on = 'ad8b797d89c0' +depends_on = "ad8b797d89c0" def upgrade(): conn = op.get_bind() - op.execute( - sa.text( - """ + op.execute(sa.text(""" DELETE FROM gn_permissions.t_objects WHERE code_object = 'MONITORINGS_INDIVIDUALS' - """ - ) - ) + """)) def downgrade(): conn = op.get_bind() - op.execute( - sa.text( - """ + op.execute(sa.text(""" INSERT INTO gn_permissions.t_objects (id_object, code_object, description_object) VALUES(nextval('gn_permissions.t_objects_id_object_seq'::regclass), 'MONITORINGS_INDIVIDUALS', 'Permissions sur les individus'); - """ - ) - ) + """)) diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index 8f413ff5a..15582cec9 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -35,7 +35,7 @@ corIndividualModule, ) from geonature.core.gn_meta.models import TDatasets -from geonature.core.gn_commons.models import TModules, cor_module_dataset +from geonature.core.gn_commons.models import TModules from geonature.core.gn_permissions.tools import has_any_permissions_by_action from pypnusershub.db.models import User @@ -639,12 +639,6 @@ class TMonitoringModules(TModules, PermissionModel, MonitoringQuery): # viewonly=True, ) - datasets = DB.relationship( - "TDatasets", - secondary=cor_module_dataset, - join_depth=0, - overlaps="modules", - ) types_site = DB.relationship( "BibTypeSite", secondary=cor_module_type, diff --git a/backend/gn_module_monitoring/routes/individuals.py b/backend/gn_module_monitoring/routes/individuals.py index 4deb95e67..5924a477d 100644 --- a/backend/gn_module_monitoring/routes/individuals.py +++ b/backend/gn_module_monitoring/routes/individuals.py @@ -34,9 +34,9 @@ methods=["GET"], defaults={"object_type": "individual"}, ) -@check_cruved_scope("R", object_code="MONITORINGS_INDIVIDUALS") +@check_cruved_scope("R", object_code="INDIVIDUALS") def get_individuals(object_type, module_code=None): - object_code = "MONITORINGS_INDIVIDUALS" + object_code = "INDIVIDUALS" params = MultiDict(request.args) limit, page = get_limit_page(params=params) sort_label, sort_dir = get_sort( @@ -77,7 +77,7 @@ def get_individuals(object_type, module_code=None): @blueprint.route( "/individuals/", methods=["DELETE"], defaults={"object_type": "individual"} ) -@check_cruved_scope("D", get_scope=True, object_code="MONITORINGS_INDIVIDUALS") +@check_cruved_scope("D", get_scope=True, object_code="INDIVIDUALS") def delete_individual(scope, _id: int, object_type: str): individual = db.get_or_404(TMonitoringIndividuals, _id) if not individual.has_instance_permission(scope=scope): diff --git a/backend/gn_module_monitoring/tests/conftest.py b/backend/gn_module_monitoring/tests/conftest.py index fe5d5363c..99283253e 100644 --- a/backend/gn_module_monitoring/tests/conftest.py +++ b/backend/gn_module_monitoring/tests/conftest.py @@ -1,4 +1,5 @@ import pytest +from geonature.tests.fixtures import * +from geonature.tests.fixtures import _session, app, _app, users from geonature.tests.test_monitoring import markings, nomenclature_type_markings from gn_module_monitoring.tests.fixtures import * -from geonature.tests.test_users_menu import tlist diff --git a/backend/gn_module_monitoring/tests/fixtures/__init__.py b/backend/gn_module_monitoring/tests/fixtures/__init__.py index 24fe23b00..b844216be 100644 --- a/backend/gn_module_monitoring/tests/fixtures/__init__.py +++ b/backend/gn_module_monitoring/tests/fixtures/__init__.py @@ -1,8 +1,9 @@ from .generic import * from .individual import * -from .module import * +from .monitoring_modules import * from .site import * from .sites_groups import * from .visit import * from .observation import * from .marking import * +from .type_site import * diff --git a/backend/gn_module_monitoring/tests/fixtures/individual.py b/backend/gn_module_monitoring/tests/fixtures/individual.py index 8618c2459..e9cd3fdd1 100644 --- a/backend/gn_module_monitoring/tests/fixtures/individual.py +++ b/backend/gn_module_monitoring/tests/fixtures/individual.py @@ -9,8 +9,6 @@ from geonature.core.gn_monitoring.models import corIndividualModule from gn_module_monitoring.monitoring.models import TMonitoringIndividuals -from geonature.tests.fixtures import users - SPECIES = "Athene noctua" diff --git a/backend/gn_module_monitoring/tests/fixtures/module.py b/backend/gn_module_monitoring/tests/fixtures/module.py index 79ac5393c..f85c8153e 100644 --- a/backend/gn_module_monitoring/tests/fixtures/module.py +++ b/backend/gn_module_monitoring/tests/fixtures/module.py @@ -101,7 +101,7 @@ def install_monitoring_module(module_code, type_site, users): "MONITORINGS_GRP_SITES", "MONITORINGS_SITES", "MONITORINGS_VISITES", - "MONITORINGS_INDIVIDUALS", + "INDIVIDUALS", "MONITORINGS_MARKINGS", "ALL", ] diff --git a/backend/gn_module_monitoring/tests/fixtures/monitoring_modules.py b/backend/gn_module_monitoring/tests/fixtures/monitoring_modules.py new file mode 100644 index 000000000..dbe8db96b --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/monitoring_modules.py @@ -0,0 +1,265 @@ +import pytest +import pytest +import shutil + +from uuid import uuid4 +from pathlib import Path +from flask import current_app + +from sqlalchemy import select + +from geonature.utils.env import db, BACKEND_DIR +from geonature.core.gn_permissions.models import ( + PermAction, + PermObject, + Permission, +) + +from apptax.taxonomie.models import BibListes +from pypnusershub.db.models import UserList + +from gn_module_monitoring.monitoring.models import TMonitoringModules +from gn_module_monitoring.command.cmd import ( + cmd_add_update_import_on_protocole, + cmd_install_monitoring_module, +) +from gn_module_monitoring.monitoring.models import TMonitoringModules +from gn_module_monitoring.tests.fixtures.generic import add_user_permission + + +@pytest.fixture +def install_module_test(types_site, users): + install_monitoring_module("test", types_site, users) + + +@pytest.fixture +def install_module_test_with_config(install_module_test): + module_test = db.session.execute( + select(TMonitoringModules).where(TMonitoringModules.module_code == "test") + ).scalar_one_or_none() + + with db.session.begin_nested(): + module_test.id_list_taxonomy = db.session.scalar(select(BibListes.id_liste).limit(1)) + module_test.id_list_observer = db.session.scalar(select(UserList.id_liste).limit(1)) + module_test.taxonomy_display_field_name = "nom_vern,lb_nom" + db.session.add(module_test) + runner = current_app.test_cli_runner() + result = runner.invoke(cmd_add_update_import_on_protocole, ["test"]) + assert result.exit_code == 0 + + +@pytest.fixture +def install_module_test_indi(types_site, data_individuals, users): + install_monitoring_module("test_indi", types_site, users) + + module_indi = db.session.execute( + select(TMonitoringModules).where(TMonitoringModules.module_code == "test_indi") + ).scalar_one_or_none() + module_data = [] + with db.session.begin_nested(): + for individual in data_individuals: + individual.modules.append(module_indi) + db.session.add(individual) + module_data.append(individual) + return module_data + + +def install_monitoring_module(module_code, type_site, users): + # Copy des fichiers du module de test + path_gn_monitoring = Path(__file__).absolute().parent.parent.parent.parent.parent + path_module_test = path_gn_monitoring / Path(f"contrib/{module_code}") + path_gn_monitoring = BACKEND_DIR / Path(f"media/monitorings/{module_code}") + shutil.copytree(src=str(path_module_test), dst=str(path_gn_monitoring), dirs_exist_ok=True) + + # Installation du module + runner = current_app.test_cli_runner() + result = runner.invoke(cmd_install_monitoring_module, [module_code]) + print(f"Result of command: {result.output}") + assert result.exit_code == 0 + if "Installation annulée" in result.output: + raise Exception(f"Installation annulée: {result.output}") + + # Association du module aux types de site existant + module = db.session.execute( + select(TMonitoringModules).where(TMonitoringModules.module_code == module_code) + ).scalar_one() + with db.session.begin_nested(): + module.types_site = list(type_site.values()) + db.session.add(module) + + # Association des permissions aux différents utilisateurs + users_to_create = [ + ("noright_user", 0), + ("stranger_user", 2), + ("associate_user", 2), + ("self_user", 1), + ("user", 2), + ("admin_user", 3), + ] + + type_code_object = [ + "MONITORINGS_MODULES", + "MONITORINGS_GRP_SITES", + "MONITORINGS_SITES", + "MONITORINGS_VISITES", + "MONITORINGS_INDIVIDUALS", + "MONITORINGS_MARKINGS", + "ALL", + ] + + for username, scope in users_to_create: + for code_object in type_code_object: + add_user_permission( + module.module_code, + users[username], + scope=scope, + type_code_object=code_object, + code_action="CRUVD", + ) + + # This is required because the first call to get_config during the install command cannot get the site types + # (because the module does not exist yet in the DB) but this incomplete config is still registered with the cache. + from gn_module_monitoring.config.repositories import get_config + + get_config(module_code, force=True) + + +@pytest.fixture +def monitoring_module(types_site, users): + t_monitoring_module = TMonitoringModules( + module_code="TEST", + uuid_module_complement=uuid4(), + module_label="test", + active_frontend=True, + active_backend=False, + b_synthese=False, + module_path="test", + types_site=list(types_site.values()), + ) + + with db.session.begin_nested(): + db.session.add(t_monitoring_module) + # Set module Permission + + actions = { + code: db.session.execute( + select(PermAction).where(PermAction.code_action == code) + ).scalar_one() + for code in "CRUVED" + } + + type_code_object = [ + "MONITORINGS_MODULES", + "MONITORINGS_GRP_SITES", + "MONITORINGS_SITES", + "MONITORINGS_VISITES", + ] + for co in type_code_object: + object_all = db.session.execute( + select(PermObject).where(PermObject.code_object == co) + ).scalar_one() + + for action in actions.values(): + for obj in [object_all] + t_monitoring_module.objects: + permission = Permission( + role=users["admin_user"], + action=action, + module=t_monitoring_module, + object=obj, + scope_value=None, + sensitivity_filter=None, + ) + db.session.add(permission) + + return t_monitoring_module + + +@pytest.fixture +def monitoring_module_wo_types_site(): + t_monitoring_module = TMonitoringModules( + module_code=uuid4(), + module_label="NoType", + active_frontend=True, + active_backend=False, + module_path="NoType", + b_synthese=False, + ) + + with db.session.begin_nested(): + db.session.add(t_monitoring_module) + + return t_monitoring_module + + +@pytest.fixture +def modules_with_and_without_permission(types_site, users): + user = users["admin_user"] + + module_with_perm = TMonitoringModules( + module_code="WITH_PERM", + module_label="Module With Perm", + uuid_module_complement=uuid4(), + module_path="with_perm", + active_frontend=True, + active_backend=False, + types_site=list(types_site.values()), + ) + + module_without_perm = TMonitoringModules( + module_code="NO_PERM", + module_label="Module Without Perm", + uuid_module_complement=uuid4(), + module_path="no_perm", + active_frontend=True, + active_backend=False, + types_site=list(types_site.values()), + ) + + with db.session.begin_nested(): + db.session.add_all([module_with_perm, module_without_perm]) + + # Ajout de droits complets sur tous les objets (y compris VISITES) + for type_code_object in [ + "MONITORINGS_MODULES", + "MONITORINGS_GRP_SITES", + "MONITORINGS_SITES", + "MONITORINGS_VISITES", + ]: + add_user_permission( + module_with_perm.module_code, user, 3, type_code_object, code_action="R" + ) + + # Ajout de droits limités pour module_without_perm (pas MONITORINGS_VISITES) + for type_code_object in ["MONITORINGS_MODULES", "MONITORINGS_GRP_SITES", "MONITORINGS_SITES"]: + add_user_permission( + module_without_perm.module_code, user, 3, type_code_object, code_action="R" + ) + + return {"with_perm": module_with_perm, "without_perm": module_without_perm} + + +@pytest.fixture +def modules_with_permissions_and_different_types(types_site, users): + admin_user = users["admin_user"] + modules = {} + + for label, type_site in types_site.items(): + module = TMonitoringModules( + module_code=f"MOD_{label}", + module_label=f"Module {label}", + module_path=f"path_{label}", + uuid_module_complement=uuid4(), + active_frontend=True, + active_backend=True, + types_site=[type_site], + ) + + with db.session.begin_nested(): + db.session.add(module) + add_user_permission( + module.module_code, admin_user, 3, "MONITORINGS_VISITES", code_action="R" + ) + + modules[label] = module + + return {"admin_user": admin_user, "modules": modules} diff --git a/backend/gn_module_monitoring/tests/test_commands/test_commands.py b/backend/gn_module_monitoring/tests/test_commands/test_commands.py index 8970655b5..80c3196e2 100644 --- a/backend/gn_module_monitoring/tests/test_commands/test_commands.py +++ b/backend/gn_module_monitoring/tests/test_commands/test_commands.py @@ -1,3 +1,6 @@ +from gn_module_monitoring.tests.fixtures.monitoring_modules import ( + install_monitoring_module, +) import pytest import json from pathlib import Path @@ -34,8 +37,6 @@ from sqlalchemy import insert from geonature.core.imports.models import TImports -from gn_module_monitoring.tests.fixtures.module import install_monitoring_module - class TestCommands: def test_install_monitoring_module(self, install_module_test): @@ -209,7 +210,7 @@ def test_cmd_add_module_protocol_fields(self, install_module_test_with_config): assert "observation" in entities assert "visit" in entities - inspector = inspect(DB.engine) + inspector = inspect(DB.session.connection()) result = inspector.has_table(destination.table_name, schema="gn_imports") assert result == True diff --git a/backend/gn_module_monitoring/tests/test_import/conftest.py b/backend/gn_module_monitoring/tests/test_import/conftest.py index 8714a1ac5..bf76b3d65 100644 --- a/backend/gn_module_monitoring/tests/test_import/conftest.py +++ b/backend/gn_module_monitoring/tests/test_import/conftest.py @@ -1,3 +1,2 @@ from geonature.utils.env import db from geonature.tests.imports.fixtures import * # Import all base import fixtures -from geonature.tests.fixtures import * # Import conftest fixtures diff --git a/backend/gn_module_monitoring/tests/test_import/test_imports_monitoring.py b/backend/gn_module_monitoring/tests/test_import/test_imports_monitoring.py index 8babe3562..5973d3828 100644 --- a/backend/gn_module_monitoring/tests/test_import/test_imports_monitoring.py +++ b/backend/gn_module_monitoring/tests/test_import/test_imports_monitoring.py @@ -45,9 +45,9 @@ def install_test_module_with_import(install_module_test): site_fields = db.session.scalars( sa.select(BibFields).where( BibFields.id_destination - == sa.select(Destination.id_destination).where( - Destination.module.has(TModules.module_code == "test") - ) + == sa.select(Destination.id_destination) + .where(Destination.module.has(TModules.module_code == "test")) + .scalar_subquery() ) ).all() @@ -214,7 +214,7 @@ class TestImportMonitoring: "autogenerate, import_file_name,fieldmapping_preset_name", [(False, "valid_hierarchy_comma.csv", None)], ) - def test_import_valid_file(self, datasets, imported_import): + def test_import_valid_file(self, imported_import): assert_import_errors( imported_import, set([]), @@ -262,7 +262,7 @@ def test_import_valid_file(self, datasets, imported_import): "autogenerate, import_file_name,fieldmapping_preset_name", [(False, "bad_cdnom.csv", None)], ) - def test_import_bad_cdnom_file(self, datasets, imported_import): + def test_import_bad_cdnom_file(self, imported_import): errors = { ( error.type.name, diff --git a/conftest.py b/conftest.py deleted file mode 100644 index 694a7c75a..000000000 --- a/conftest.py +++ /dev/null @@ -1,11 +0,0 @@ -from geonature.tests.fixtures import * -from geonature.tests.fixtures import _session, app, _app, users - -pytest_plugins = [ - "gn_module_monitoring.tests.fixtures.generic", - "gn_module_monitoring.tests.fixtures.module", - "gn_module_monitoring.tests.fixtures.site", - "gn_module_monitoring.tests.fixtures.sites_groups", - "gn_module_monitoring.tests.fixtures.type_site", - "gn_module_monitoring.tests.fixtures.visit", -] diff --git a/docs/individuals.md b/docs/individuals.md index dbb596b39..349c1c55f 100644 --- a/docs/individuals.md +++ b/docs/individuals.md @@ -185,4 +185,4 @@ Elle permet de désactiver la saisie du `cd_nom` au profit de l'individu. ## Permissions Comme tout objet Monitoring, des permissions seront ajoutées à l'installation -pour CRUD sur les objets `MONITORINGS_INDIVIDUALS` et `MONITORINGS_MARKINGS`. +pour CRUD sur les objets `INDIVIDUALS` et `MONITORINGS_MARKINGS`. From 4a01d7ff950da65d7afeb4e2e8f8ef9fde3191a4 Mon Sep 17 00:00:00 2001 From: jacquesfize Date: Fri, 25 Sep 2026 15:07:54 +0200 Subject: [PATCH 4/4] small refact after review --- .github/workflows/pytest.yml | 2 +- .../bbdd47114b4d_rename_individuals_permissions.py | 4 ---- backend/gn_module_monitoring/monitoring/models.py | 6 ++---- .../tests/fixtures/monitoring_modules.py | 2 +- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 33b51afc8..6149bb4a5 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -23,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - geonature_ref: ["sqla2"] # Mettre version compatible + geonature_ref: ["develop"] # Mettre version compatible uses: pnx-si/geonature/.github/workflows/gn-module-pytest.yml@develop with: geonature_ref: ${{ matrix.geonature_ref }} diff --git a/backend/gn_module_monitoring/migrations/bbdd47114b4d_rename_individuals_permissions.py b/backend/gn_module_monitoring/migrations/bbdd47114b4d_rename_individuals_permissions.py index f7839f760..9e07b74f9 100644 --- a/backend/gn_module_monitoring/migrations/bbdd47114b4d_rename_individuals_permissions.py +++ b/backend/gn_module_monitoring/migrations/bbdd47114b4d_rename_individuals_permissions.py @@ -17,8 +17,6 @@ def upgrade(): - conn = op.get_bind() - op.execute(sa.text(""" DELETE FROM gn_permissions.t_objects WHERE code_object = 'MONITORINGS_INDIVIDUALS' @@ -26,8 +24,6 @@ def upgrade(): def downgrade(): - conn = op.get_bind() - op.execute(sa.text(""" INSERT INTO gn_permissions.t_objects (id_object, code_object, description_object) VALUES(nextval('gn_permissions.t_objects_id_object_seq'::regclass), 'MONITORINGS_INDIVIDUALS', 'Permissions sur les individus'); diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index 15582cec9..77d26a367 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -469,7 +469,7 @@ class TMonitoringSitesGroups(DB.Model, PermissionModel, SitesGroupsQuery): digitiser = DB.relationship( User, primaryjoin=(User.id_role == id_digitiser), foreign_keys=[id_digitiser] ) - uuid_sites_group: Mapped[Optional[Any]] = mapped_column(UUID(as_uuid=True), default=uuid4) + uuid_sites_group: Mapped[Any] = mapped_column(UUID(as_uuid=True), default=uuid4) sites_group_name: Mapped[Optional[str]] = mapped_column(DB.Unicode) sites_group_code: Mapped[Optional[str]] = mapped_column(DB.Unicode) @@ -589,9 +589,7 @@ class TMonitoringModules(TModules, PermissionModel, MonitoringQuery): unique=True, ) - uuid_module_complement: Mapped[Optional[Any]] = mapped_column( - UUID(as_uuid=True), default=uuid4 - ) + uuid_module_complement: Mapped[Any] = mapped_column(UUID(as_uuid=True), default=uuid4) id_list_observer: Mapped[Optional[int]] = mapped_column(DB.Integer) id_list_taxonomy: Mapped[Optional[int]] = mapped_column(DB.Integer) diff --git a/backend/gn_module_monitoring/tests/fixtures/monitoring_modules.py b/backend/gn_module_monitoring/tests/fixtures/monitoring_modules.py index dbe8db96b..53c447194 100644 --- a/backend/gn_module_monitoring/tests/fixtures/monitoring_modules.py +++ b/backend/gn_module_monitoring/tests/fixtures/monitoring_modules.py @@ -102,7 +102,7 @@ def install_monitoring_module(module_code, type_site, users): "MONITORINGS_GRP_SITES", "MONITORINGS_SITES", "MONITORINGS_VISITES", - "MONITORINGS_INDIVIDUALS", + "INDIVIDUALS", "MONITORINGS_MARKINGS", "ALL", ]