From e717216f631d500b737d0b46e1866a64c442a324 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Thu, 22 Dec 2022 14:11:36 +0100 Subject: [PATCH] Feat/site type categories and module categorie (#18) * feat(api): add association table with alembic Add model in backend and alembic migration Reviewed-by: andriac [Refs ticket]: #3 * test: WIP add test to see new relationship Adding test to see if categories are showing up when we call module Reviewed-by: andriacap [Refs ticket]: #3 * feat: add type site - categorie relation WIP - add selectfield to get type site in admin module Reviewed-by: andriac [Refs ticket]: #3 * feat(api): Flask admin and routes categories Clean code for change label list and form selectfield for the BibCategorieView in Flask Admin Add utils routes to get all subtable relationship in order to get back the label type site Review-by: andriac [Refs ticket]: #3 * refactor: remove paginate_nested For depth in as_dict() * test: fix tests due to as_dict depth * style: applied black and isort * chore: remove unused import Co-authored-by: Andria Capai --- ...a54bafb13ce8_create_cor_module_category.py | 47 +++++++++++++++++++ ...bafb13ce8_create_cor_site_type_category.py | 47 +++++++++++++++++++ .../gn_module_monitoring/monitoring/admin.py | 30 +++++++++++- .../gn_module_monitoring/monitoring/models.py | 44 ++++++++++++++++- backend/gn_module_monitoring/routes/site.py | 8 ++-- .../tests/fixtures/__init__.py | 0 .../tests/fixtures/module.py | 21 +++++++++ .../tests/fixtures/site.py | 14 +++++- .../test_monitoring/test_models/__init__.py | 0 .../test_models/test_module.py | 11 +++++ .../test_monitoring/test_routes/test_site.py | 6 +-- backend/gn_module_monitoring/utils/routes.py | 9 ++-- 12 files changed, 220 insertions(+), 17 deletions(-) create mode 100644 backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_category.py create mode 100644 backend/gn_module_monitoring/migrations/e64bafb13ce8_create_cor_site_type_category.py create mode 100644 backend/gn_module_monitoring/tests/fixtures/__init__.py create mode 100644 backend/gn_module_monitoring/tests/fixtures/module.py create mode 100644 backend/gn_module_monitoring/tests/test_monitoring/test_models/__init__.py create mode 100644 backend/gn_module_monitoring/tests/test_monitoring/test_models/test_module.py diff --git a/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_category.py b/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_category.py new file mode 100644 index 000000000..491198526 --- /dev/null +++ b/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_category.py @@ -0,0 +1,47 @@ +"""create_cor_module_category + +Revision ID: a54bafb13ce8 +Revises: +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 = "f24adb481f54" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" +referent_schema = "gn_commons" + + +def upgrade(): + op.create_table( + "cor_module_categorie", + sa.Column( + "id_categorie", + sa.Integer(), + sa.ForeignKey( + f"{monitorings_schema}.bib_categorie_site.id_categorie", + name="fk_cor_module_categorie_id_categorie", + 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_categorie_id_module", + ondelete="CASCADE", + onupdate="CASCADE", + ), nullable=False), + sa.PrimaryKeyConstraint("id_categorie", "id_module", name="pk_cor_module_categorie"), + schema=monitorings_schema, + ) + + +def downgrade(): + op.drop_table("cor_module_categorie", schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/migrations/e64bafb13ce8_create_cor_site_type_category.py b/backend/gn_module_monitoring/migrations/e64bafb13ce8_create_cor_site_type_category.py new file mode 100644 index 000000000..2dfcf3412 --- /dev/null +++ b/backend/gn_module_monitoring/migrations/e64bafb13ce8_create_cor_site_type_category.py @@ -0,0 +1,47 @@ +"""create_cor_site_type_category + +Revision ID: e64bafb13ce8 +Revises: +Create Date: 2022-12-06 16:18:24.512562 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "e64bafb13ce8" +down_revision = "a54bafb13ce8" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" +referent_schema = "ref_nomenclatures" + + +def upgrade(): + op.create_table( + "cor_site_type_categorie", + sa.Column( + "id_categorie", + sa.Integer(), + sa.ForeignKey( + f"{monitorings_schema}.bib_categorie_site.id_categorie", + name="fk_cor_site_type_categorie_id_categorie", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=False, + ), + sa.Column("id_nomenclature", sa.Integer(),sa.ForeignKey( + f"{referent_schema}.t_nomenclatures.id_nomenclature", + name="fk_cor_site_type_categorie_id_type", + ondelete="CASCADE", + onupdate="CASCADE", + ), nullable=False), + sa.PrimaryKeyConstraint("id_categorie", "id_nomenclature", name="pk_cor_site_type_categorie"), + schema=monitorings_schema, + ) + + +def downgrade(): + op.drop_table("cor_site_type_categorie", schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/monitoring/admin.py b/backend/gn_module_monitoring/monitoring/admin.py index b71e81f9e..e9f2559a4 100644 --- a/backend/gn_module_monitoring/monitoring/admin.py +++ b/backend/gn_module_monitoring/monitoring/admin.py @@ -1,6 +1,7 @@ from flask_admin.contrib.sqla import ModelView from geonature.core.admin.admin import CruvedProtectedMixin - +from geonature.core.gn_commons.models import TNomenclatures +from geonature.utils.env import DB from gn_module_monitoring.monitoring.models import BibCategorieSite @@ -16,3 +17,30 @@ class BibCategorieSiteView(CruvedProtectedMixin, ModelView): def __init__(self, session, **kwargs): # Référence au model utilisé super(BibCategorieSiteView, self).__init__(BibCategorieSite, session, **kwargs) + + def get_only_type_site_asc(): + return ( + DB.session.query(TNomenclatures) + .filter(TNomenclatures.id_type == 116) + .order_by(TNomenclatures.label_fr.asc()) + ) + + def get_label_fr_nomenclature(x): + return x.label_fr + + def list_label_site_type_formatter(view, _context, model, _name): + return [item.label_fr for item in model.site_type] + + # Nom de colonne user friendly + column_labels = dict(site_type="Type de site") + # Description des colonnes + column_descriptions = dict(site_type="Type de site à choisir en lien avec la catégorie") + + column_hide_backrefs = False + + form_args = dict( + site_type=dict(query_factory=get_only_type_site_asc, get_label=get_label_fr_nomenclature) + ) + + column_list = ("label", "config", "site_type") + column_formatters = dict(site_type=list_label_site_type_formatter) diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index 9851d0a3e..5559eb782 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -11,7 +11,7 @@ from sqlalchemy.ext.hybrid import hybrid_property - +from pypnnomenclature.models import TNomenclatures, BibNomenclaturesTypes from geonature.core.gn_commons.models import TMedias from geonature.core.gn_monitoring.models import TBaseSites, TBaseVisits from geonature.core.gn_meta.models import TDatasets @@ -20,6 +20,35 @@ from pypnusershub.db.models import User from geonature.core.gn_monitoring.models import corVisitObserver +cor_module_categorie = DB.Table( + "cor_module_categorie", + DB.Column( + "id_module", + DB.Integer, + DB.ForeignKey("gn_commons.t_modules.id_module"), + primary_key=True, + ), + DB.Column( + "id_categorie", + DB.Integer, + DB.ForeignKey("gn_monitoring.bib_categorie_site.id_categorie"), + primary_key=True, + ), schema="gn_monitoring") + +cor_site_type_categorie = DB.Table( + "cor_site_type_categorie", + DB.Column( + "id_nomenclature", + DB.Integer, + DB.ForeignKey("ref_nomenclatures.t_nomenclatures.id_nomenclature"), + primary_key=True, + ), + DB.Column( + "id_categorie", + DB.Integer, + DB.ForeignKey("gn_monitoring.bib_categorie_site.id_categorie"), + primary_key=True, + ), schema="gn_monitoring") @serializable class BibCategorieSite(DB.Model): @@ -28,8 +57,13 @@ class BibCategorieSite(DB.Model): id_categorie = DB.Column(DB.Integer, primary_key=True, nullable=False, unique=True) label = DB.Column(DB.String, nullable=False) config = DB.Column(JSONB) + site_type = DB.relationship( + "TNomenclatures", + secondary=cor_site_type_categorie, + lazy="joined", + ) - + @serializable class TMonitoringObservationDetails(DB.Model): __tablename__ = "t_observation_details" @@ -315,6 +349,12 @@ class TMonitoringModules(TModules): lazy="joined", ) + categories = DB.relationship( + "BibCategorieSite", + secondary=cor_module_categorie, + lazy="joined" + ) + data = DB.Column(JSONB) diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py index 9adf65bec..b55d2a07e 100644 --- a/backend/gn_module_monitoring/routes/site.py +++ b/backend/gn_module_monitoring/routes/site.py @@ -1,5 +1,3 @@ -from typing import Tuple - from flask import request from flask.json import jsonify from geonature.core.gn_monitoring.models import TBaseSites @@ -7,8 +5,7 @@ from gn_module_monitoring.blueprint import blueprint from gn_module_monitoring.monitoring.models import BibCategorieSite -from gn_module_monitoring.utils.routes import (filter_params, get_limit_offset, - paginate) +from gn_module_monitoring.utils.routes import filter_params, get_limit_offset, paginate @blueprint.route("/sites/categories", methods=["GET"]) @@ -18,7 +15,8 @@ def get_categories(): query = filter_params(query=BibCategorieSite.query, params=params) query = query.order_by(BibCategorieSite.id_categorie) - return paginate(query=query, object_name="categories", limit=limit, page=page) + + return paginate(query=query, object_name="categories", limit=limit, page=page, depth=1) @blueprint.route("/sites/categories/", methods=["GET"]) diff --git a/backend/gn_module_monitoring/tests/fixtures/__init__.py b/backend/gn_module_monitoring/tests/fixtures/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/fixtures/module.py b/backend/gn_module_monitoring/tests/fixtures/module.py new file mode 100644 index 000000000..6e0a5b1ac --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/module.py @@ -0,0 +1,21 @@ +import pytest +from geonature.core.gn_commons.models.base import TModules +from geonature.utils.env import db + +from gn_module_monitoring.monitoring.models import TMonitoringModules +from gn_module_monitoring.tests.fixtures.site import categories + + +@pytest.fixture +def monitoring_module(module, categories): + id_module = TModules.query.filter(TModules.id_module == module.id_module).one().id_module + t_monitoring_module = TMonitoringModules() + + module_data = {"id_module": id_module, "categories": list(categories.values())} + t_monitoring_module.from_dict(module_data) + # monitoring = TMonitoringModules(id_module=id_module, categories=list(categories.values())) + monitoring = t_monitoring_module + with db.session.begin_nested(): + db.session.add(monitoring) + + return monitoring diff --git a/backend/gn_module_monitoring/tests/fixtures/site.py b/backend/gn_module_monitoring/tests/fixtures/site.py index a74b4d22c..4325c0bf5 100644 --- a/backend/gn_module_monitoring/tests/fixtures/site.py +++ b/backend/gn_module_monitoring/tests/fixtures/site.py @@ -9,8 +9,18 @@ @pytest.fixture() -def categories(): - categories = [{"label": "gite", "config": {}}, {"label": "eolienne", "config": {}}] +def site_type(): + return TNomenclatures.query.filter( + BibNomenclaturesTypes.mnemonique == "TYPE_SITE", TNomenclatures.mnemonique == "Grotte" + ).one() + + +@pytest.fixture() +def categories(site_type): + categories = [ + {"label": "gite", "config": {}, "site_type": [site_type]}, + {"label": "eolienne", "config": {}, "site_type": [site_type]}, + ] categories = {cat["label"]: BibCategorieSite(**cat) for cat in categories} diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_models/__init__.py b/backend/gn_module_monitoring/tests/test_monitoring/test_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_module.py b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_module.py new file mode 100644 index 000000000..e156a1614 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_module.py @@ -0,0 +1,11 @@ +import pytest + +from gn_module_monitoring.tests.fixtures.module import monitoring_module +from gn_module_monitoring.tests.fixtures.site import categories + + +@pytest.mark.usefixtures("temporary_transaction") +class TestModule: + def test_module(self, monitoring_module): + cateogories = monitoring_module.categories + assert False diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_site.py b/backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_site.py index 56deccbd5..9c7f8ac16 100644 --- a/backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_site.py +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_site.py @@ -1,7 +1,7 @@ import pytest from flask import url_for -from gn_module_monitoring.tests.fixtures.site import categories, sites +from gn_module_monitoring.tests.fixtures.site import categories, site_type, sites @pytest.mark.usefixtures("client_class", "temporary_transaction") @@ -20,13 +20,13 @@ def test_get_categories(self, categories): r = self.client.get(url_for("monitorings.get_categories")) assert r.json["count"] >= len(categories) - assert all([cat.as_dict() in r.json["categories"] for cat in categories.values()]) + assert all([cat.as_dict(depth=1) in r.json["categories"] for cat in categories.values()]) def test_get_categories_label(self, categories): label = list(categories.keys())[0] r = self.client.get(url_for("monitorings.get_categories"), query_string={"label": label}) - assert categories[label].as_dict() in r.json["categories"] + assert categories[label].as_dict(depth=1) in r.json["categories"] def test_get_sites(self, sites): r = self.client.get(url_for("monitorings.get_sites")) diff --git a/backend/gn_module_monitoring/utils/routes.py b/backend/gn_module_monitoring/utils/routes.py index e2b0fa48c..b6c16a1d7 100644 --- a/backend/gn_module_monitoring/utils/routes.py +++ b/backend/gn_module_monitoring/utils/routes.py @@ -10,17 +10,18 @@ def get_limit_offset(params: MultiDict) -> Tuple[int]: return params.pop("limit", 50), params.pop("offset", 1) -def paginate(query: Query, object_name: str, limit: int, page: int) -> Response: +def paginate(query: Query, object_name: str, limit: int, page: int, depth: int = 0) -> Response: result = query.paginate(page=page, error_out=False, max_per_page=limit) data = { - object_name: [res.as_dict() for res in result.items], + object_name: [res.as_dict(depth=depth) for res in result.items], "count": result.total, "limit": limit, "offset": page - 1, } return jsonify(data) -def filter_params(query: Query, params: MultiDict) -> Query: + +def filter_params(query: Query, params: MultiDict) -> Query: if len(params) != 0: query = query.filter_by(**params) - return query \ No newline at end of file + return query