diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index cbdb334fb3..94ce100e14 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -73,9 +73,15 @@ jobs: working-directory: ./backend - name: Install database run: | - install/03b_populate_db_for_test.sh + install/03b_populate_db.sh env: GEONATURE_CONFIG_FILE: config/test_config.toml + srid_local: 2154 + install_bdc_statuts: true + add_sample_data: true + install_sig_layers: true + install_grid_layer_5: true + install_ref_sensitivity: true # FRONTEND - name: Cache node modules uses: actions/cache@v3 diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index d533b3d286..b3d42d0d52 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -89,9 +89,15 @@ jobs: GEONATURE_CONFIG_FILE: config/test_config.toml - name: Install database run: | - install/03b_populate_db_for_test.sh + install/03b_populate_db.sh env: GEONATURE_CONFIG_FILE: config/test_config.toml + srid_local: 2154 + install_bdc_statuts: true + add_sample_data: true + install_sig_layers: true + install_grid_layer_5: true + install_ref_sensitivity: true - name: Show database status run: | geonature db status diff --git a/VERSION b/VERSION index 53fdb123b9..d1cdec8ed6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.13.1 \ No newline at end of file +2.13.2 \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 3a2808fbe7..27e2581570 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1.2 -FROM python:3.9-bullseye AS build +FROM python:3.11-bookworm AS build ENV PIP_ROOT_USER_ACTION=ignore RUN --mount=type=cache,target=/root/.cache \ diff --git a/backend/dependencies/UsersHub-authentification-module b/backend/dependencies/UsersHub-authentification-module index 35427a1e80..344bdda4c7 160000 --- a/backend/dependencies/UsersHub-authentification-module +++ b/backend/dependencies/UsersHub-authentification-module @@ -1 +1 @@ -Subproject commit 35427a1e80c1e0fa00bd5e0ec7a007569ed5ddc3 +Subproject commit 344bdda4c702e3ce21fd4600735cca6f78acbb70 diff --git a/backend/geonature/app.py b/backend/geonature/app.py index b2a7b4a018..f95db8ebf5 100755 --- a/backend/geonature/app.py +++ b/backend/geonature/app.py @@ -107,19 +107,18 @@ def create_app(with_external_mods=True): app.wsgi_app = SchemeFix(app.wsgi_app, scheme=config.get("PREFERRED_URL_SCHEME")) app.wsgi_app = ProxyFix(app.wsgi_app, x_host=1) app.wsgi_app = RequestID(app.wsgi_app) - if app.config["APPLICATION_ROOT"] != "/": - app.wsgi_app = DispatcherMiddleware( - Response("Not Found", status=404), - {app.config["APPLICATION_ROOT"].rstrip("/"): app.wsgi_app}, - ) - if config.get("CUSTOM_STATIC_FOLDER"): app.wsgi_app = SharedDataMiddleware( app.wsgi_app, { - "/static": config["CUSTOM_STATIC_FOLDER"], + app.static_url_path: config["CUSTOM_STATIC_FOLDER"], }, ) + if app.config["APPLICATION_ROOT"] != "/": + app.wsgi_app = DispatcherMiddleware( + Response("Not Found", status=404), + {app.config["APPLICATION_ROOT"].rstrip("/"): app.wsgi_app}, + ) app.json = MyJSONProvider(app) diff --git a/backend/geonature/celery_app.py b/backend/geonature/celery_app.py index de70b720c8..d614ba1354 100644 --- a/backend/geonature/celery_app.py +++ b/backend/geonature/celery_app.py @@ -14,6 +14,7 @@ def __call__(self, *args, **kwargs): app.Task = ContextTask +app.conf.imports += ("geonature.tasks",) app.conf.imports += tuple( [ep.module for dist in iter_modules_dist() for ep in dist.entry_points.select(name="tasks")] ) diff --git a/backend/geonature/core/gn_commons/routes.py b/backend/geonature/core/gn_commons/routes.py index 6d8d7aff13..790f378415 100644 --- a/backend/geonature/core/gn_commons/routes.py +++ b/backend/geonature/core/gn_commons/routes.py @@ -209,13 +209,13 @@ def get_t_mobile_apps(): if app.relative_path_apk: relative_apk_path = Path("mobile", app.relative_path_apk) app_dict["url_apk"] = url_for("media", filename=str(relative_apk_path), _external=True) - relative_settings_path = relative_apk_path.parent / "settings.json" - app_dict["url_settings"] = url_for( - "media", filename=relative_settings_path, _external=True - ) - settings_file = Path(current_app.config["MEDIA_FOLDER"]) / relative_settings_path - with settings_file.open() as f: - app_dict["settings"] = json.load(f) + relative_settings_path = Path(f"mobile/{app.app_code.lower()}/settings.json") + app_dict["url_settings"] = url_for( + "media", filename=relative_settings_path, _external=True + ) + settings_file = Path(current_app.config["MEDIA_FOLDER"]) / relative_settings_path + with settings_file.open() as f: + app_dict["settings"] = json.load(f) mobile_apps.append(app_dict) if len(mobile_apps) == 1: return mobile_apps[0] diff --git a/backend/geonature/core/gn_meta/models.py b/backend/geonature/core/gn_meta/models.py index ad9b11efb6..5b2c8f646e 100644 --- a/backend/geonature/core/gn_meta/models.py +++ b/backend/geonature/core/gn_meta/models.py @@ -253,10 +253,12 @@ def _get_read_scope(self, user=None): cruved = get_scopes_by_action(id_role=user.id_role, module_code="METADATA") return cruved["R"] - def _get_create_scope(self, module_code, user=None): + def _get_create_scope(self, module_code, user=None, object_code=None): if user is None: user = g.current_user - cruved = get_scopes_by_action(id_role=user.id_role, module_code=module_code) + cruved = get_scopes_by_action( + id_role=user.id_role, module_code=module_code, object_code=object_code + ) return cruved["C"] def filter_by_scope(self, scope, user=None): @@ -366,14 +368,14 @@ def filter_by_readable(self, user=None): """ return self.filter_by_scope(self._get_read_scope(user)) - def filter_by_creatable(self, module_code, user=None): + def filter_by_creatable(self, module_code, user=None, object_code=None): """ Return all dataset where user have read rights minus those who user to not have create rigth """ query = self.filter(TDatasets.modules.any(module_code=module_code)) scope = self._get_read_scope(user) - create_scope = self._get_create_scope(module_code, user=user) + create_scope = self._get_create_scope(module_code, user=user, object_code=object_code) if create_scope < scope: scope = create_scope return query.filter_by_scope(scope) diff --git a/backend/geonature/core/gn_meta/routes.py b/backend/geonature/core/gn_meta/routes.py index bf03454541..fd5d98749d 100644 --- a/backend/geonature/core/gn_meta/routes.py +++ b/backend/geonature/core/gn_meta/routes.py @@ -65,6 +65,7 @@ from werkzeug.datastructures import Headers from geonature.core.gn_permissions import decorators as permissions from geonature.core.gn_permissions.tools import get_scopes_by_action +from geonature.core.gn_permissions.models import TObjects from geonature.core.gn_meta.mtd import mtd_utils import geonature.utils.filemanager as fm import geonature.utils.utilsmails as mail @@ -99,15 +100,25 @@ def get_datasets(): .. :quickref: Metadata; :query boolean active: filter on active fiel + :query string create: filter on C permission for the module_code specified + (we can specify the object_code by adding a . between both) :query int id_acquisition_framework: get only dataset of given AF :returns: `list` """ params = MultiDict(request.args) + if request.is_json: + params.update(request.json) fields = params.get("fields", type=str, default=[]) if fields: fields = fields.split(",") if "create" in params: - query = TDatasets.query.filter_by_creatable(params.pop("create")) + create = params.pop("create").split(".") + if len(create) > 1: + query = TDatasets.query.filter_by_creatable( + module_code=create[0], object_code=create[1] + ) + else: + query = TDatasets.query.filter_by_creatable(module_code=create[0]) else: query = TDatasets.query.filter_by_readable() diff --git a/backend/geonature/core/gn_meta/schemas.py b/backend/geonature/core/gn_meta/schemas.py index 0567d2acd7..db22198464 100644 --- a/backend/geonature/core/gn_meta/schemas.py +++ b/backend/geonature/core/gn_meta/schemas.py @@ -68,18 +68,22 @@ class Meta: def module_input(self, item, original, many, **kwargs): if "modules" in item: for i, module in enumerate(original.modules): + if not hasattr(module, "generate_input_url_for_dataset"): + continue + object_code = getattr(module.generate_input_url_for_dataset, "object_code", "ALL") create_scope = get_scopes_by_action( - id_role=g.current_user.id_role, module_code=module.module_code + id_role=g.current_user.id_role, + module_code=module.module_code, + object_code=object_code, )["C"] if not original.has_instance_permission(create_scope): continue - if hasattr(module, "generate_input_url_for_dataset"): - item["modules"][i].update( - { - "input_url": module.generate_input_url_for_dataset(original), - "input_label": module.generate_input_url_for_dataset.label, - } - ) + item["modules"][i].update( + { + "input_url": module.generate_input_url_for_dataset(original), + "input_label": module.generate_input_url_for_dataset.label, + } + ) return item # retro-compatibility with mobile app diff --git a/backend/geonature/core/gn_synthese/models.py b/backend/geonature/core/gn_synthese/models.py index 14b9f0d347..820d286ed0 100644 --- a/backend/geonature/core/gn_synthese/models.py +++ b/backend/geonature/core/gn_synthese/models.py @@ -525,6 +525,7 @@ class VSyntheseForWebApp(DB.Model): unique_id_sinp = DB.Column(UUID(as_uuid=True)) unique_id_sinp_grp = DB.Column(UUID(as_uuid=True)) id_source = DB.Column(DB.Integer, nullable=False) + id_module = DB.Column(DB.Integer) entity_source_pk_value = DB.Column(DB.Integer) id_dataset = DB.Column(DB.Integer) dataset_name = DB.Column(DB.Integer) diff --git a/backend/geonature/core/gn_synthese/routes.py b/backend/geonature/core/gn_synthese/routes.py index edf04e4722..470cf67d36 100644 --- a/backend/geonature/core/gn_synthese/routes.py +++ b/backend/geonature/core/gn_synthese/routes.py @@ -1098,6 +1098,7 @@ def create_report(permissions): synthese = Synthese.query.options( Load(Synthese).raiseload("*"), + joinedload("nomenclature_sensitivity"), joinedload("cor_observers"), joinedload("digitiser"), joinedload("dataset"), diff --git a/backend/geonature/core/gn_synthese/utils/query_select_sqla.py b/backend/geonature/core/gn_synthese/utils/query_select_sqla.py index 963bac7a89..6234abec71 100644 --- a/backend/geonature/core/gn_synthese/utils/query_select_sqla.py +++ b/backend/geonature/core/gn_synthese/utils/query_select_sqla.py @@ -20,6 +20,8 @@ from geoalchemy2.shape import from_shape from geonature.utils.env import DB + +from geonature.core.gn_commons.models import TModules from geonature.core.gn_synthese.models import ( CorObserverSynthese, CorAreaSynthese, @@ -372,17 +374,16 @@ def filter_other_filters(self, user): self.query = self.query.where(self.model.id_dataset.in_(formated_datasets)) if "date_min" in self.filters: self.query = self.query.where(self.model.date_min >= self.filters.pop("date_min")) - if "date_max" in self.filters: # set the date_max at 23h59 because a hour can be set in timestamp date_max = datetime.datetime.strptime(self.filters.pop("date_max"), "%Y-%m-%d") date_max = date_max.replace(hour=23, minute=59, second=59) self.query = self.query.where(self.model.date_max <= date_max) - if "id_source" in self.filters: self.add_join(TSources, self.model.id_source, TSources.id_source) - self.query = self.query.where(self.model.id_source == self.filters.pop("id_source")) - + self.query = self.query.where(self.model.id_source.in_(self.filters.pop("id_source"))) + if "id_module" in self.filters: + self.query = self.query.where(self.model.id_module.in_(self.filters.pop("id_module"))) if "id_acquisition_framework" in self.filters: if hasattr(self.model, "id_acquisition_framework"): self.query = self.query.where( diff --git a/backend/geonature/migrations/versions/446e902a14e7_add_id_module_to_v_synthese_for_web_app.py b/backend/geonature/migrations/versions/446e902a14e7_add_id_module_to_v_synthese_for_web_app.py new file mode 100644 index 0000000000..bbd8250046 --- /dev/null +++ b/backend/geonature/migrations/versions/446e902a14e7_add_id_module_to_v_synthese_for_web_app.py @@ -0,0 +1,183 @@ +"""add id_module to v_synthese_for_web_app + +Revision ID: 446e902a14e7 +Revises: f1dd984bff97 +Create Date: 2023-09-25 10:09:39.126531 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "446e902a14e7" +down_revision = "f1dd984bff97" +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute( + """ + DROP VIEW gn_synthese.v_synthese_for_web_app; + """ + ) + + op.execute( + """ + CREATE OR REPLACE VIEW gn_synthese.v_synthese_for_web_app AS + SELECT s.id_synthese, + s.unique_id_sinp, + s.unique_id_sinp_grp, + s.id_source, + s.entity_source_pk_value, + s.count_min, + s.count_max, + s.nom_cite, + s.meta_v_taxref, + s.sample_number_proof, + s.digital_proof, + s.non_digital_proof, + s.altitude_min, + s.altitude_max, + s.depth_min, + s.depth_max, + s.place_name, + s.precision, + s.the_geom_4326, + public.ST_asgeojson(the_geom_4326), + s.date_min, + s.date_max, + s.validator, + s.validation_comment, + s.observers, + s.id_digitiser, + s.determiner, + s.comment_context, + s.comment_description, + s.meta_validation_date, + s.meta_create_date, + s.meta_update_date, + s.last_action, + d.id_dataset, + d.dataset_name, + d.id_acquisition_framework, + s.id_nomenclature_geo_object_nature, + s.id_nomenclature_info_geo_type, + s.id_nomenclature_grp_typ, + s.grp_method, + s.id_nomenclature_obs_technique, + s.id_nomenclature_bio_status, + s.id_nomenclature_bio_condition, + s.id_nomenclature_naturalness, + s.id_nomenclature_exist_proof, + s.id_nomenclature_valid_status, + s.id_nomenclature_diffusion_level, + s.id_nomenclature_life_stage, + s.id_nomenclature_sex, + s.id_nomenclature_obj_count, + s.id_nomenclature_type_count, + s.id_nomenclature_sensitivity, + s.id_nomenclature_observation_status, + s.id_nomenclature_blurring, + s.id_nomenclature_source_status, + s.id_nomenclature_determination_method, + s.id_nomenclature_behaviour, + s.reference_biblio, + sources.name_source, + sources.url_source, + t.cd_nom, + t.cd_ref, + t.nom_valide, + t.lb_nom, + t.nom_vern, + s.id_module + FROM gn_synthese.synthese s + JOIN taxonomie.taxref t ON t.cd_nom = s.cd_nom + JOIN gn_meta.t_datasets d ON d.id_dataset = s.id_dataset + JOIN gn_synthese.t_sources sources ON sources.id_source = s.id_source; + """ + ) + + +def downgrade(): + op.execute( + """ + DROP VIEW gn_synthese.v_synthese_for_web_app; + """ + ) + + op.execute( + """ + CREATE OR REPLACE VIEW gn_synthese.v_synthese_for_web_app AS + SELECT s.id_synthese, + s.unique_id_sinp, + s.unique_id_sinp_grp, + s.id_source, + s.entity_source_pk_value, + s.count_min, + s.count_max, + s.nom_cite, + s.meta_v_taxref, + s.sample_number_proof, + s.digital_proof, + s.non_digital_proof, + s.altitude_min, + s.altitude_max, + s.depth_min, + s.depth_max, + s.place_name, + s.precision, + s.the_geom_4326, + public.ST_asgeojson(the_geom_4326), + s.date_min, + s.date_max, + s.validator, + s.validation_comment, + s.observers, + s.id_digitiser, + s.determiner, + s.comment_context, + s.comment_description, + s.meta_validation_date, + s.meta_create_date, + s.meta_update_date, + s.last_action, + d.id_dataset, + d.dataset_name, + d.id_acquisition_framework, + s.id_nomenclature_geo_object_nature, + s.id_nomenclature_info_geo_type, + s.id_nomenclature_grp_typ, + s.grp_method, + s.id_nomenclature_obs_technique, + s.id_nomenclature_bio_status, + s.id_nomenclature_bio_condition, + s.id_nomenclature_naturalness, + s.id_nomenclature_exist_proof, + s.id_nomenclature_valid_status, + s.id_nomenclature_diffusion_level, + s.id_nomenclature_life_stage, + s.id_nomenclature_sex, + s.id_nomenclature_obj_count, + s.id_nomenclature_type_count, + s.id_nomenclature_sensitivity, + s.id_nomenclature_observation_status, + s.id_nomenclature_blurring, + s.id_nomenclature_source_status, + s.id_nomenclature_determination_method, + s.id_nomenclature_behaviour, + s.reference_biblio, + sources.name_source, + sources.url_source, + t.cd_nom, + t.cd_ref, + t.nom_valide, + t.lb_nom, + t.nom_vern + FROM gn_synthese.synthese s + JOIN taxonomie.taxref t ON t.cd_nom = s.cd_nom + JOIN gn_meta.t_datasets d ON d.id_dataset = s.id_dataset + JOIN gn_synthese.t_sources sources ON sources.id_source = s.id_source; + """ + ) diff --git a/backend/geonature/tasks/__init__.py b/backend/geonature/tasks/__init__.py new file mode 100644 index 0000000000..5d4f5354b8 --- /dev/null +++ b/backend/geonature/tasks/__init__.py @@ -0,0 +1,9 @@ +from celery.signals import task_postrun + +from geonature.utils.env import db +from geonature.utils.celery import celery_app + + +@task_postrun.connect +def close_session(*args, **kwargs): + db.session.remove() diff --git a/backend/geonature/tests/fixtures.py b/backend/geonature/tests/fixtures.py index 411d934f75..f7a9e9783a 100644 --- a/backend/geonature/tests/fixtures.py +++ b/backend/geonature/tests/fixtures.py @@ -62,6 +62,8 @@ "perm_object", "notifications_enabled", "celery_eager", + "sources_modules", + "modules", ] @@ -86,17 +88,55 @@ def app(): transaction.rollback() # rollback all database changes +def create_module(module_code, module_label, module_path, active_frontend, active_backend): + return TModules( + module_code=module_code, + module_label=module_label, + module_path=module_path, + active_frontend=active_frontend, + active_backend=active_backend, + ) + + +@pytest.fixture() +def modules(): + dict_module_to_create = { + 0: { + "module_code": "MODULE_TEST_1", + "module_label": "module_test_1", + "module_path": "module_test_1", + "active_frontend": True, + "active_backend": True, + }, + 1: { + "module_code": "MODULE_TEST_2", + "module_label": "module_test_2", + "module_path": "module_test_2", + "active_frontend": True, + "active_backend": True, + }, + } + modules = [] + for key, module in dict_module_to_create.items(): + modules.append( + create_module( + module["module_code"], + module["module_label"], + module["module_path"], + module["active_frontend"], + module["active_backend"], + ) + ) + with db.session.begin_nested(): + db.session.add_all(modules) + return modules + + @pytest.fixture(scope="function") def module(users): other_module = TModules.query.filter_by(module_code="GEONATURE").one() with db.session.begin_nested(): - new_module = TModules( - module_code="MODULE_1", - module_label="module_1", - module_path="module_1", - active_frontend=True, - active_backend=False, - ) + new_module = create_module("MODULE_1", "module_1", "module_1", True, False) db.session.add(new_module) # Copy perission from another module with db.session.begin_nested(): @@ -305,11 +345,22 @@ def source(): return source +@pytest.fixture() +def sources_modules(modules): + sources = [] + for name_source, module in [("source test 1", modules[0]), ("source test 2", modules[1])]: + sources.append(TSources(name_source=name_source, module=module)) + with db.session.begin_nested(): + db.session.add_all(sources) + return sources + + def create_synthese(geom, taxon, user, dataset, source, uuid, cor_observers, **kwargs): now = datetime.datetime.now() return Synthese( id_source=source.id_source, + id_module=source.id_module, unique_id_sinp=uuid, dataset=dataset, digitiser=user, @@ -327,22 +378,22 @@ def create_synthese(geom, taxon, user, dataset, source, uuid, cor_observers, **k @pytest.fixture() -def synthese_data(app, users, datasets, source): +def synthese_data(app, users, datasets, source, sources_modules): point1 = Point(5.92, 45.56) point2 = Point(-1.54, 46.85) point3 = Point(-3.486786, 48.832182) data = {} with db.session.begin_nested(): - for name, cd_nom, point, ds, comment_description in [ - ("obs1", 713776, point1, datasets["own_dataset"], "obs1"), - ("obs2", 212, point2, datasets["own_dataset"], "obs2"), - ("obs3", 2497, point3, datasets["own_dataset"], "obs3"), - ("p1_af1", 713776, point1, datasets["belong_af_1"], "p1_af1"), - ("p1_af1_2", 212, point1, datasets["belong_af_1"], "p1_af1_2"), - ("p1_af2", 212, point1, datasets["belong_af_2"], "p1_af2"), - ("p2_af2", 2497, point2, datasets["belong_af_2"], "p2_af2"), - ("p2_af1", 2497, point2, datasets["belong_af_1"], "p2_af1"), - ("p3_af3", 2497, point3, datasets["belong_af_3"], "p3_af3"), + for name, cd_nom, point, ds, comment_description, source_m in [ + ("obs1", 713776, point1, datasets["own_dataset"], "obs1", sources_modules[0]), + ("obs2", 212, point2, datasets["own_dataset"], "obs2", sources_modules[0]), + ("obs3", 2497, point3, datasets["own_dataset"], "obs3", sources_modules[1]), + ("p1_af1", 713776, point1, datasets["belong_af_1"], "p1_af1", sources_modules[1]), + ("p1_af1_2", 212, point1, datasets["belong_af_1"], "p1_af1_2", sources_modules[1]), + ("p1_af2", 212, point1, datasets["belong_af_2"], "p1_af2", sources_modules[1]), + ("p2_af2", 2497, point2, datasets["belong_af_2"], "p2_af2", source), + ("p2_af1", 2497, point2, datasets["belong_af_1"], "p2_af1", source), + ("p3_af3", 2497, point3, datasets["belong_af_3"], "p3_af3", source), ]: unique_id_sinp = ( "f4428222-d038-40bc-bc5c-6e977bbbc92b" if not data else func.uuid_generate_v4() @@ -356,7 +407,7 @@ def synthese_data(app, users, datasets, source): taxon, users["self_user"], ds, - source, + source_m, unique_id_sinp, [users["admin_user"], users["user"]], **kwargs, diff --git a/backend/geonature/tests/test_gn_meta.py b/backend/geonature/tests/test_gn_meta.py index a8fca40ad6..706fedaac0 100644 --- a/backend/geonature/tests/test_gn_meta.py +++ b/backend/geonature/tests/test_gn_meta.py @@ -679,6 +679,25 @@ def test_get_dataset_filter_module_code(self, users, datasets, module): assert expected_ds.issubset(filtered_ds) assert datasets["own_dataset"].id_dataset not in filtered_ds + def test_get_dataset_filter_create(self, users, datasets, module): + set_logged_user_cookie(self.client, users["admin_user"]) + + response = self.client.get( + url_for("gn_meta.get_datasets"), + json={"module_code": module.module_code, "create": module.module_code}, + ) + + response_with_object = self.client.get( + url_for("gn_meta.get_datasets"), + json={"module_code": module.module_code, "create": module.module_code + ".ALL"}, + ) + + expected_ds = {datasets["with_module_1"].id_dataset} + filtered_ds = {ds["id_dataset"] for ds in response.json} + assert response.json == response_with_object.json + assert expected_ds.issubset(filtered_ds) + assert datasets["own_dataset"].id_dataset not in filtered_ds + def test_get_dataset_search(self, users, datasets, module): set_logged_user_cookie(self.client, users["admin_user"]) ds = datasets["with_module_1"] diff --git a/backend/geonature/tests/test_reports.py b/backend/geonature/tests/test_reports.py index fdcb54e124..d217949696 100644 --- a/backend/geonature/tests/test_reports.py +++ b/backend/geonature/tests/test_reports.py @@ -70,6 +70,16 @@ def test_create_report(self, synthese_data, users): data = {"content": "comment 4", "type": "discussion"} response = self.client.post(url_for(url), data=data) assert response.status_code == BadRequest.code + # TEST VALID - ADD PIN + response = self.client.post( + url_for(url), data={"item": id_synthese, "content": "", "type": "pin"} + ) + assert response.status_code == 204 + # TEST INVALID - ADD PIN + response = self.client.post( + url_for(url), data={"item": id_synthese, "content": "", "type": "pin"} + ) + assert response.status_code == 409 def test_delete_report(self, reports_data, users): # NO AUTHENT diff --git a/backend/geonature/tests/test_synthese.py b/backend/geonature/tests/test_synthese.py index b5c3089557..138d90bf1a 100644 --- a/backend/geonature/tests/test_synthese.py +++ b/backend/geonature/tests/test_synthese.py @@ -26,7 +26,7 @@ from pypnnomenclature.models import TNomenclatures, BibNomenclaturesTypes from .fixtures import * -from .fixtures import create_synthese +from .fixtures import create_synthese, create_module from .utils import jsonschema_definitions @@ -325,7 +325,7 @@ def test_get_observations_for_web_filter_id_source(self, users, synthese_data, s id_source = source.id_source url = url_for("gn_synthese.get_observations_for_web") - filters = {"id_source": id_source} + filters = {"id_source": [id_source]} r = self.client.get(url, json=filters) expected_data = { @@ -336,6 +336,40 @@ def test_get_observations_for_web_filter_id_source(self, users, synthese_data, s response_data = {feature["properties"]["id"] for feature in r.json["features"]} assert expected_data.issubset(response_data) + @pytest.mark.parametrize( + "module_label_to_filter,expected_length", + [(["MODULE_TEST_1"], 2), (["MODULE_TEST_2"], 4), (["MODULE_TEST_1", "MODULE_TEST_2"], 6)], + ) + def test_get_observations_for_web_filter_source_by_id_module( + self, + users, + synthese_data, + sources_modules, + modules, + module_label_to_filter, + expected_length, + ): + set_logged_user_cookie(self.client, users["self_user"]) + + id_modules_selected = [] + for module in modules: + for module_to_filt in module_label_to_filter: + if module.module_code == module_to_filt: + id_modules_selected.append(module.id_module) + + url = url_for("gn_synthese.get_observations_for_web") + filters = {"id_module": id_modules_selected} + r = self.client.get(url, json=filters) + + expected_data = { + synthese.id_synthese + for synthese in synthese_data.values() + if synthese.id_module in id_modules_selected + } + response_data = {feature["properties"]["id"] for feature in r.json["features"]} + assert expected_data.issubset(response_data) + assert len(response_data) == expected_length + @pytest.mark.parametrize( "observer_input,expected_length_synthese", [("Vincent", 1), ("CamillĂ©", 2), ("Camille, Elie", 2), ("Jane Doe", 0)], @@ -445,7 +479,7 @@ def test_export(self, users): ) assert response.status_code == 200 - def test_export_observations(self, users, synthese_data, synthese_sensitive_data): + def test_export_observations(self, users, synthese_data, synthese_sensitive_data, modules): data_synthese = synthese_data.values() data_synthese_sensitive = synthese_sensitive_data.values() list_id_synthese = [obs_data_synthese.id_synthese for obs_data_synthese in data_synthese] diff --git a/backend/geonature/utils/config_schema.py b/backend/geonature/utils/config_schema.py index 395bc4e347..e0b9ebdfcf 100644 --- a/backend/geonature/utils/config_schema.py +++ b/backend/geonature/utils/config_schema.py @@ -102,6 +102,8 @@ class MailConfig(Schema): class CeleryConfig(Schema): broker_url = fields.String(load_default="redis://localhost:6379/0") result_backend = fields.String(load_default="redis://localhost:6379/0") + enable_utc = fields.Boolean(load_default=False) + timezone = fields.String(load_default=None) class AccountManagement(Schema): diff --git a/backend/requirements-dependencies.in b/backend/requirements-dependencies.in index 2c0a963d4c..78d0c4637d 100644 --- a/backend/requirements-dependencies.in +++ b/backend/requirements-dependencies.in @@ -1,4 +1,4 @@ -pypnusershub>=1.6.10,<2 +pypnusershub>=1.6.11,<2 pypnnomenclature>=1.5.4,<2 pypn_habref_api>=0.3.2,<1 utils-flask-sqlalchemy-geo>=0.2.8,<1 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5621818727..fe69dcdc63 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,35 @@ CHANGELOG ========= +2.13.2 (2023-09-28) +------------------- + +**🚀 NouveautĂ©s** + +- [SynthĂšse] Ajout d'un filtre par module de provenance (#2670, par @andriacap) + +**🐛 Corrections** + +- Correction des dĂ©connexions non effectives dans les versions 2.13.0 et 2.13.1 (#2682, par @TheoLechemia) +- Correction des permissions vĂ©rifiĂ©es pour pouvoir supprimer un signalement en prenant en compte le C du module Validation, et non pas le R qui n'existe pas sur ce module (#2710, par @Pierre-Narcisi) +- Correction de l'API des applications mobiles quand le chemin de l'APK est absolu (#2708, par @joelclement) +- Correction des permissions des listes de JDD dans les modules de saisie (Occtax, Occhab, Import) en prenant en compte la portĂ©e du C du module, et pas seulement du R du module MĂ©tadonnĂ©es (#2712, par @Pierre-Narcisi) +- Utilisation de l'heure locale du serveur pour lancer les taches Celery (#2725, par @bouttier) +- Fermeture des connexions Ă  la BDD Ă  la fin des taches Celery (#2724, par @bouttier) +- Correction de l'affichage du bouton permettant d'importer directement depuis la fiche d'un JDD, nĂ©cessitant la version 2.2.3 du module Import (#2713, par @bouttier) + +**đŸ’» DĂ©veloppement** + +- Ajout du thĂšme Bootstrap au composant `datalist` (#2727, par @TheoLechemia) +- Docker : utilisation de python 3.11 (#2728, par @bouttier) +- DĂ©placement du `DispatcherMiddleware` aprĂšs les fichiers statiques customisĂ©s (#2720, par @bouttier) +- Suppression du script `03b_populate_db_for_test.sh` (#2726, par @bouttier) + +**📝 Documentation** + +- Mise Ă  jour de la documentation suite aux Ă©volutions des permissions dans la 2.13.0 (par @camillemonchicourt) + + 2.13.1 (2023-09-15) ------------------- @@ -88,8 +117,8 @@ C'est la maniĂšre la plus simple de dĂ©ployer GeoNature avec ses 4 modules exter - Ajout d'un script `install/03b_populate_db_for_test.sh` pouvant ĂȘtre utilisĂ© par la CI de test des modules GeoNature (#2544) - Ajout d'un script `install/assets/docker_startup.sh` pour lancer les migrations Alembic depuis le docker de GeoNature (#2544) - CrĂ©ation d'un fichier `install/assets/db/add_pg_extensions.sql` regroupant la crĂ©ation des extensions PostgreSQL (#2544) -- ation de `APPLICATION_ROOT` pour qu'il fonctionne en mode dĂ©veloppement (#2546) -- ation des modĂšles de la SynthĂšse pour prendre en compte les valeurs par dĂ©faut des nomenclatures (#2524) +- AmĂ©lioration de `APPLICATION_ROOT` pour qu'il fonctionne en mode dĂ©veloppement (#2546) +- AmĂ©lioration des modĂšles de la SynthĂšse pour prendre en compte les valeurs par dĂ©faut des nomenclatures (#2524) - Meilleure portabilitĂ© des scripts dans les diffĂ©rents systĂšmes Unix (#2435) - Mise Ă  jour des dĂ©pendances Python (#2596) - Documentation de dĂ©veloppement des permissions (#2585) @@ -171,7 +200,7 @@ Si vous utilisiez des champs additionnels avec des checkbox, lors de leur change - Correction et ations des performances des recherches par statut de protection, notamment quand elles sont associĂ©es Ă  une recherche gĂ©ographique (#2450, par @amandine-sahl) - Correction d’une rĂ©gression des performances lors de la rĂ©cupĂ©ration des JDD (#2462, par @mvergez) -- Correction de jointures manquantes pour le calcule des permissions lors de la rĂ©cupĂ©ration des JDD (#2463, par @mvergez) +- Correction de jointures manquantes pour le calcul des permissions lors de la rĂ©cupĂ©ration des JDD (#2463, par @mvergez) - Correction des champs additionnels de type liste (#2447, par @TheoLechemia) - Correction d’une incompatibilitĂ© Python 3.7 (#2464, par @TheoLechemia) - Suppression en cascade des permissions et associations aux sites lors de la suppresion d’un module (#2466, par @jbrieuclp & @VincentCauchois) diff --git a/docs/admin-manual.rst b/docs/admin-manual.rst index f1a305b9f3..9b694e4b1b 100644 --- a/docs/admin-manual.rst +++ b/docs/admin-manual.rst @@ -6,7 +6,7 @@ Architecture GeoNature possĂšde une architecture modulaire et s'appuie sur plusieurs "services" indĂ©pendants pour fonctionner : -- UsersHub et son sous-module d'authentification Flask (https://github.com/PnX-SI/UsersHub-authentification-module) sont utilisĂ©s pour gĂ©rer le schĂ©ma de BDD ``ref_users`` (actuellement nommĂ© ``utilisateurs``) et l'authentification. UsersHub permet une gestion centralisĂ©e de ses utilisateurs (listes, organismes, droits), utilisable par les diffĂ©rentes applications de son systĂšme d'information. +- UsersHub et son sous-module d'authentification Flask (https://github.com/PnX-SI/UsersHub-authentification-module) sont utilisĂ©s pour gĂ©rer le schĂ©ma de BDD ``ref_users`` (actuellement nommĂ© ``utilisateurs``) et l'authentification. UsersHub permet une gestion centralisĂ©e de ses utilisateurs (listes, organismes, applications), utilisable par les diffĂ©rentes applications de son systĂšme d'informations. - TaxHub (https://github.com/PnX-SI/TaxHub) est utilisĂ© pour la gestion du schĂ©ma de BDD ``ref_taxonomy`` (actuellement nommĂ© ``taxonomie``). L'API de TaxHub est utilisĂ©e pour rĂ©cupĂ©rer des informations sur les espĂšces et la taxonomie en gĂ©nĂ©ral. - Un sous-module Flask (https://github.com/PnX-SI/Nomenclature-api-module/) a Ă©tĂ© crĂ©Ă© pour une gestion centralisĂ©e des nomenclatures (https://github.com/PnX-SI/Nomenclature-api-module/), il pilote le schĂ©ma ``ref_nomenclature``. - ``ref_geo`` est le schĂ©ma de base de donnĂ©es qui gĂšre le rĂ©fĂ©rentiel gĂ©ographique. Il est utilisĂ© pour gĂ©rer les zonages, les communes, le MNT, le calcul automatique d'altitude et les intersections spatiales. @@ -42,7 +42,7 @@ SchĂ©ma simplifiĂ© de la BDD : - En bleu, les schĂ©mas des protocoles et sources de donnĂ©es - En vert, les schĂ©mas des applications pouvant interagir avec le coeur de GeoNature -Depuis la version 2.0.0-rc.4, il faut noter que les droits (CRUVED) ont Ă©tĂ© retirĂ©s du schĂ©ma ``utilisateurs`` (``ref_users``) de UsersHub pour l'intĂ©grer dans GeoNature dans un schĂ©ma ``gn_permissions``, Ă  ajouter en rose. +Depuis la version 2.0.0-rc.4, il faut noter que les permissions (CRUVED) ont Ă©tĂ© retirĂ©es du schĂ©ma ``utilisateurs`` (``ref_users``) de UsersHub pour l'intĂ©grer dans GeoNature dans un schĂ©ma ``gn_permissions``, Ă  ajouter en rose. ModĂšle simplifiĂ© de la BDD (2017-12-15) : @@ -392,13 +392,13 @@ AccĂšs Ă  GeoNature et CRUVED Les comptes des utilisateurs, leur mot de passe, email, groupes et leur accĂšs Ă  l'application GeoNature sont gĂ©rĂ©s de maniĂšre centralisĂ©e dans l'application UsersHub. Pour qu'un rĂŽle (utilisateur ou groupe) ait accĂšs Ă  GeoNature, il faut lui attribuer un profil de "Lecteur" dans l'application GeoNature, grĂące Ă  l'application UsersHub. -La gestion des droits (permissions) des rĂŽles, spĂ©cifique Ă  GeoNature, est ensuite gĂ©rĂ©e dans un schĂ©ma (``gn_permissions``) depuis le module ADMIN de GeoNature. Voir https://github.com/PnX-SI/GeoNature/issues/2605. +La gestion des droits (permissions) des rĂŽles, spĂ©cifique Ă  GeoNature, est ensuite gĂ©rĂ©e dans un schĂ©ma (``gn_permissions``) depuis le module ADMIN de GeoNature. Voir https://docs.geonature.fr/user-manual.html#gestion-des-permissions. -La gestion des droits dans GeoNature, comme dans beaucoup d'applications, est liĂ©e Ă  des actions (Create / Read / Update / Delete aka CRUD). Pour les besoins mĂ©tiers de l'application nous avons rajoutĂ© deux actions : "Exporter" et "Valider" (non utilisĂ©e), ce qui donne le CRUVED : Create / Read / Update / Validate / Export / Delete. +La gestion des permissions dans GeoNature, comme dans beaucoup d'applications, est liĂ©e Ă  des actions (Create / Read / Update / Delete aka CRUD). Pour les besoins mĂ©tier de l'application, nous avons rajoutĂ© deux actions : "Exporter" et "Valider" (non utilisĂ©e), ce qui donne le CRUVED : Create / Read / Update / Validate / Export / Delete. Chaque module peut utiliser toutes ou certaines de ces actions. -Selon les modules, on peut appliquer des filtres sur ces actions. Notamment des filtres d'appartenance (portĂ©es / scope) : +Selon les modules, on peut appliquer des filtres sur ces actions. Notamment des filtres d'appartenance (portĂ©e / scope) : - PortĂ©e 1 = Seulement mes donnĂ©es. Cela concerne les donnĂ©es sur lesquels je suis : @@ -423,10 +423,15 @@ Cas particulier de l'action "C" | Dans les modules de saisie (comme Occtax), on veut que des utilisateurs puissent saisir uniquement dans certains JDD. | La liste des JDD ouverts Ă  la saisie est contrĂŽlĂ©e par l'action "CREATE" du module dans lequel on se trouve. -| Comme il n'est pas "normal" de pouvoir saisir dans des JDD sur lesquels on n'a pas les droits de lecture, la portĂ©e de l'action "CREATE" vient simplement rĂ©duire la liste des JDD sur lesquels on a les droits de lecture ("READ"). -| MĂȘme si la portĂ©e de l'action "CREATE" sur le module est supĂ©rieure Ă  celle de l'action "READ", l'utilisateur ne verra que les JDD sur lesquels il a des droits de lecture +| Comme il n'est pas "normal" de pouvoir saisir dans des JDD sur lesquels on n'a pas les permissions de lecture, la portĂ©e de l'action "CREATE" vient simplement rĂ©duire la liste des JDD sur lesquels on a les permissions de lecture ("READ"). +| MĂȘme si la portĂ©e de l'action "CREATE" sur le module est supĂ©rieure Ă  celle de l'action "READ", l'utilisateur ne verra que les JDD sur lesquels il a des permissions de lecture. -Une commande dĂ©diĂ©e permet d'ajouter tous les droits sur tous les modules Ă  un groupe ou utilisateur ayant le rĂŽle d'administrateur. Cette commande peut ĂȘtre relancĂ©e aprĂšs l'installation d'un nouveau module : +Permissions d'administrateur +```````````````````````````` + +Chaque module (ou sous-module) dĂ©finit ses permissions disponibles lors de son installation. Cependant une fois installĂ©, aucun utilisateur n'a de permission sur un nouveau module. Il faut les dĂ©finir explicitement. + +Une commande dĂ©diĂ©e permet d'ajouter toutes les permissions sur tous les modules Ă  un groupe ou utilisateur ayant le rĂŽle d'administrateur. Cette commande peut ĂȘtre relancĂ©e aprĂšs l'installation d'un nouveau module : .. code-block:: bash @@ -1490,7 +1495,8 @@ Cet espace est activable grĂące au paramĂštre ``ENABLE_USER_MANAGEMENT``. Par d AccĂšs public """""""""""" -Cette section de la documentation concerne l'implĂ©mentation d'un utilisateur-lecteur pour votre instance GeoNature. +Cette section de la documentation concerne l'implĂ©mentation d'un utilisateur gĂ©nĂ©rique et public accĂ©dant Ă  votre instance GeoNature sans authentification. +Cela ajoute sur la page d'authentification de GeoNature, un bouton "AccĂšs public" donnant accĂšs Ă  GeoNature sans authentification. Etapes : @@ -1503,7 +1509,7 @@ Etapes : - Pour GeoNature, cliquer sur le premier icĂŽne 'Voir les membres' - Cliquer sur ajouter un rĂŽle - Choisir l'utilisateur juste crĂ©Ă© - - Attribuer le rĂŽle 1, 'lecteur' + - Attribuer le rĂŽle 1, 'Lecteur' 2/ Configuration GeoNature : @@ -1516,16 +1522,16 @@ Etapes : :ref:`ExĂ©cuter les actions post-modification de la configuration `. -A ce moment-lĂ , cet utilisateur n’a aucun droit sur GeoNature. -Il s'agit maintenant de gĂ©rer ses permissions dans GeoNature mĂȘme. +A ce moment-lĂ , cet utilisateur n’a aucune permission dans GeoNature. +Il s'agit maintenant de gĂ©rer ses permissions dans GeoNature. 3/ GeoNature - Se connecter Ă  GeoNature avec un utilisateur administrateur - Aller dans le module Admin - - Cliquer sur 'Gestion des permissions' + - Cliquer sur 'Backoffice', puis "Permissions" / "Par utilisateurs" - Choisissez l'utilisateur sĂ©lectionnĂ© - - Editer le CRUVED pour chacun des modules de l'instance. Passer Ă  0 tous les droits et tous les modules devant ĂȘtre supprimĂ©s. Laisser '1' pour les modules d'intĂ©rĂȘt. + - Ajouter des permissions pour chacun des modules de l'instance auquel vous souhaitez que l'utilisateur public accĂšde AccĂšs public automatique ```````````````````````` @@ -1736,14 +1742,10 @@ La vue doit cependant contenir les champs suivants pour que les filtres de reche geom_4326, dataset_name -Attribuer des droits -"""""""""""""""""""" - -La gestion des droits (CRUVED) se fait module par module. Cependant si on ne redĂ©finit pas de droit pour un module, ce sont les droits de l'application mĂšre (GeoNature elle-mĂȘme) qui seront attribuĂ©s Ă  l'utilisateur pour l'ensemble de ses sous-modules. - -Pour ne pas afficher le module Occtax Ă  un utilisateur oĂč Ă  un groupe, il faut lui mettre l'action Read (R) Ă  0. +Attribuer des permissions +""""""""""""""""""""""""" -L'administration des droits des utilisateurs pour le module Occtax se fait dans le backoffice de gestion des permissions de GeoNature. +La gestion des permissions (CRUVED) se fait module par module, depuis le module "Admin". Dupliquer le module Occtax """""""""""""""""""""""""" diff --git a/docs/installation.rst b/docs/installation.rst index ab58280b17..6058bcbf04 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -161,14 +161,14 @@ Exemple pour le module Import : source ~/GeoNature/backend/venv/bin/activate geonature install-gn-module ~/gn_module_import/ IMPORT -Puis relancer GeoNatureet son worker : +Puis relancer GeoNature et son worker : .. code-block:: bash sudo systemctl restart geonature sudo systemctl restart geonature-worker -Aucune permission n'est dĂ©finie par dĂ©faut lors de l'installation d'un module. En tant qu'administrateur, vous pouvez une commande ajoutant tous les droits sur tous les modules Ă  un groupe ou utilisateur. Cette commande peut ĂȘtre relancĂ©e aprĂšs l'installation d'un module pour automatiquement attribuer toutes les permissions Ă  un groupe ou utilisateur administrateur : +Aucune permission n'est dĂ©finie par dĂ©faut lors de l'installation d'un module. En tant qu'administrateur, vous pouvez exĂ©cuter une commande ajoutant tous les droits sur tous les modules Ă  un groupe ou utilisateur. Cette commande peut ĂȘtre relancĂ©e aprĂšs l'installation d'un module pour automatiquement attribuer toutes les permissions Ă  un groupe ou utilisateur administrateur : .. code-block:: bash diff --git a/frontend/src/app/GN2CommonModule/form/datalist/datalist.component.html b/frontend/src/app/GN2CommonModule/form/datalist/datalist.component.html index 9ddbe125cb..76f087a1e8 100644 --- a/frontend/src/app/GN2CommonModule/form/datalist/datalist.component.html +++ b/frontend/src/app/GN2CommonModule/form/datalist/datalist.component.html @@ -1,44 +1,69 @@
- - {{ label }} - - - - - - - - {{ displayLabelFromValue(value) }} - cancel - - - - + + + {{ label }} + + + + + + + + {{ displayLabelFromValue(value) }} + cancel + + + + + {{ displayLabel(value) }} + + + help - {{ displayLabel(value) }} - - - help - + +
+ DataListComponent : Chargement en cours... + + {{ label }} + + + +
+ {{ item[keyLabel] }} +
+
+
+ +
diff --git a/frontend/src/app/GN2CommonModule/form/datalist/datalist.component.ts b/frontend/src/app/GN2CommonModule/form/datalist/datalist.component.ts index 8e5b31f66b..9d232ba808 100644 --- a/frontend/src/app/GN2CommonModule/form/datalist/datalist.component.ts +++ b/frontend/src/app/GN2CommonModule/form/datalist/datalist.component.ts @@ -19,6 +19,7 @@ import { CommonService } from '../../service/common.service'; export class DatalistComponent extends GenericFormComponent implements OnInit { formId: string; // Unique form id + @Input() designStyle: 'bootstrap' | 'material' = 'material'; @Input() values: Array; // list of choices @Input() keyLabel = 'label'; // field name for value @Input() keyValue = 'value'; // field name for label 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 f60a60c63f..3b3b024674 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 @@ -326,6 +326,7 @@ Sources des données
-
+