From 8f830340c965c6f557a23b38bbb144d001b46e78 Mon Sep 17 00:00:00 2001 From: Maxime Vergez Date: Tue, 6 Dec 2022 16:05:43 +0100 Subject: [PATCH 001/161] Merged feat/package-module-alembic commit 70b01395d5031487a4e74e9cbe60a87ceabe2a70 Author: Maxime Vergez Date: Tue Dec 6 15:22:56 2022 +0100 style: apply black commit 2badedf9976435319cc577ec3e4b861e1a4ec3c1 Author: Maxime Vergez Date: Tue Dec 6 15:11:41 2022 +0100 chore(db): removed unused import commit 57d9b523d620cc55580002a53f2883cdf692ba5f Author: Maxime Vergez Date: Tue Dec 6 14:47:48 2022 +0100 feat(db): improved & finished downgrade func commit 211780f4467f27f0e188623b03c46c25823c34bc Author: Maxime Vergez Date: Tue Dec 6 09:32:12 2022 +0100 feat(db): wip: add upgrade and began downgrade Downgrade migration need a little bit more work due to the use of sql alchemy and not plain text sql commit 0b5a3b883fe8f08e0f8d34198ab3c44b39728159 Author: Maxime Vergez Date: Mon Dec 5 11:40:32 2022 +0100 feat: packaged module --- backend/gn_module_monitoring/routes/data_utils.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/backend/gn_module_monitoring/routes/data_utils.py b/backend/gn_module_monitoring/routes/data_utils.py index 6f840827b..2c0fae73b 100644 --- a/backend/gn_module_monitoring/routes/data_utils.py +++ b/backend/gn_module_monitoring/routes/data_utils.py @@ -3,7 +3,6 @@ d'utilisateurs de nomenclature de taxonomie - TODO cache """ @@ -116,7 +115,6 @@ def get_util_nomenclature_api(code_nomenclature_type, cd_nomenclature): revoie un champ d'un object de type nomenclature à partir de son type et de son cd_nomenclature renvoie l'objet entier si field_name renseigné en paramètre de route est 'all' - :param code_nomenclature_type: :param cd_nomenclature: :return object entier si field_name = all, la valeur du champs defini par field_name sinon @@ -166,7 +164,6 @@ def get_util_from_id_api(type_util, id): """ revoie un champ d'un object de type nomenclature, taxonomy, utilisateur, ... renvoie l'objet entier si field_name renseigné en paramètre de route est 'all' - :param type_util: 'nomenclature' | 'taxonomy' | 'utilisateur' | etc.... :param id: id de l'object requis :type type_util: str @@ -210,7 +207,6 @@ def get_util_from_ids_api(type_util, ids): """ variante de get_util_from_id_api pour plusieurs id renvoie un tableau de valeur (ou de dictionnaire si key est 'all') - parametre get key: all renvoie tout l'objet sinon renvoie un champ @@ -218,13 +214,11 @@ def get_util_from_ids_api(type_util, ids): pour reformer une chaine de caractere a partir du tableau résultat de la requete si separator_out == ' ,' alors ['jean', 'pierre', 'paul'].join(separator_out) -> 'jean, pierre, paul' - :param type_util: 'nomenclature' | 'taxonomy' | 'utilisateur' :param ids: plusieurs id reliée par des '-' (ex: 1-123-3-4) :type type_util: str :type ids: str :return list si key=all ou chaine de caratere - """ field_name = request.args.get("field_name", "all") @@ -250,4 +244,4 @@ def get_util_from_ids_api(type_util, ids): return [r.as_dict() for r in res] # renvoie une chaine de caratère - return separator_out.join([r[0] for r in res]) + return separator_out.join([r[0] for r in res]) \ No newline at end of file From 8886db7584031095fef640be8ce6051b663bc5c1 Mon Sep 17 00:00:00 2001 From: Maxime Vergez Date: Fri, 9 Dec 2022 17:06:45 +0100 Subject: [PATCH 002/161] Merged feat/bib-categorie-site Squashed commit of the following: commit 2bdc22a427d9c4b74849c3828b3be6d482c03e09 Author: Maxime Vergez Date: Fri Dec 9 14:59:31 2022 +0100 chore: changed json_schema into config commit 42793894ab9459fdb6e5b5a59e26250c1085b29d Author: Maxime Vergez Date: Tue Dec 6 18:37:41 2022 +0100 feat(db): add foreign key from site to cat From t_base_sites to bib_categorie_site commit 5cb5dfa34bfc11b7eff1864867240d318cc6ad9f Author: Maxime Vergez Date: Tue Dec 6 16:47:27 2022 +0100 feat(api): add admin interface commit 559a0ec35cbe2beed58fbd017df3c993735aa585 Author: Maxime Vergez Date: Tue Dec 6 16:46:27 2022 +0100 feat(db): add bib_categorie_site table Add migration and model --- backend/gn_module_monitoring/blueprint.py | 6 ++ .../b53bafb13ce8_create_bib_categorie_site.py | 60 +++++++++++++++++++ .../gn_module_monitoring/monitoring/admin.py | 18 ++++++ 3 files changed, 84 insertions(+) create mode 100644 backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_categorie_site.py create mode 100644 backend/gn_module_monitoring/monitoring/admin.py diff --git a/backend/gn_module_monitoring/blueprint.py b/backend/gn_module_monitoring/blueprint.py index 2e56e3ce7..267b8fbba 100644 --- a/backend/gn_module_monitoring/blueprint.py +++ b/backend/gn_module_monitoring/blueprint.py @@ -4,6 +4,10 @@ """ from flask import Blueprint, current_app +from geonature.core.admin.admin import admin as flask_admin +from geonature.utils.env import DB + +from gn_module_monitoring.monitoring.admin import BibCategorieSiteView from .command.cmd import commands blueprint = Blueprint( @@ -14,3 +18,5 @@ blueprint.cli.short_help = "Commandes pour l" "administration du module MONITORINGS" for cmd in commands: blueprint.cli.add_command(cmd) + +flask_admin.add_view(BibCategorieSiteView(DB.session, name="Catégories de sites", category="Monitorings")) diff --git a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_categorie_site.py b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_categorie_site.py new file mode 100644 index 000000000..610269dd5 --- /dev/null +++ b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_categorie_site.py @@ -0,0 +1,60 @@ +"""create_bib_categorie_site + +Revision ID: b53bafb13ce8 +Revises: +Create Date: 2022-12-06 16:18:24.512562 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "b53bafb13ce8" +down_revision = "362cf9d504ec" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" + + +def upgrade(): + op.create_table( + "bib_categorie_site", + sa.Column("id_categorie", sa.Integer(), nullable=False), + sa.Column("label", sa.String(), nullable=False), + sa.Column("config", sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint("id_categorie"), + schema=monitorings_schema, + ) + op.create_index( + op.f("ix_bib_categorie_site_id"), + "bib_categorie_site", + ["id_categorie"], + unique=False, + schema=monitorings_schema, + ) + op.add_column( + "t_base_sites", + sa.Column( + "id_categorie", + sa.Integer(), + sa.ForeignKey( + f"{monitorings_schema}.bib_categorie_site.id_categorie", + name="fk_t_base_sites_id_categorie", + ondelete="CASCADE", + ), + nullable=True, # TODO: see migration? nullable is conservative here + ), + schema=monitorings_schema, + ) + + +def downgrade(): + op.drop_constraint("fk_t_base_sites_id_categorie", "t_base_sites", schema=monitorings_schema) + op.drop_column("t_base_sites", "id_categorie", schema=monitorings_schema) + op.drop_index( + op.f("ix_bib_categorie_site_id"), + table_name="bib_categorie_site", + schema=monitorings_schema, + ) + op.drop_table("bib_categorie_site", schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/monitoring/admin.py b/backend/gn_module_monitoring/monitoring/admin.py new file mode 100644 index 000000000..b71e81f9e --- /dev/null +++ b/backend/gn_module_monitoring/monitoring/admin.py @@ -0,0 +1,18 @@ +from flask_admin.contrib.sqla import ModelView +from geonature.core.admin.admin import CruvedProtectedMixin + + +from gn_module_monitoring.monitoring.models import BibCategorieSite + + +class BibCategorieSiteView(CruvedProtectedMixin, ModelView): + """ + Surcharge de l'administration des catégories de sites + """ + + module_code = "MONITORINGS" + object_code = None + + def __init__(self, session, **kwargs): + # Référence au model utilisé + super(BibCategorieSiteView, self).__init__(BibCategorieSite, session, **kwargs) From b9a23a40bd01de1922b2e4e3697912ed94934146 Mon Sep 17 00:00:00 2001 From: andriacap <111564663+andriacap@users.noreply.github.com> Date: Mon, 12 Dec 2022 11:25:24 +0100 Subject: [PATCH 003/161] feat: [6.2] Page d'accueil module monitoring layout et config title et description #2 (#15) * feat: [6.2] Page d'accueil modul monitoring layout et config title et description #2 * chore(config): applied black & removed imports --- .../gn_module_monitoring/conf_schema_toml.py | 9 ++- .../components/modules/modules.component.css | 26 ++++---- .../components/modules/modules.component.html | 59 ++++++++++++++----- .../components/modules/modules.component.ts | 29 +++++++-- frontend/app/services/config.service.ts | 6 ++ monitorings_config.toml.example | 4 ++ 6 files changed, 98 insertions(+), 35 deletions(-) diff --git a/backend/gn_module_monitoring/conf_schema_toml.py b/backend/gn_module_monitoring/conf_schema_toml.py index 3c27b1173..68963a0f8 100644 --- a/backend/gn_module_monitoring/conf_schema_toml.py +++ b/backend/gn_module_monitoring/conf_schema_toml.py @@ -4,7 +4,7 @@ Fichier à ne pas modifier. Paramètres surcouchables dans config/config_gn_module.tml """ -from marshmallow import Schema, fields, validates_schema, ValidationError +from marshmallow import Schema, fields # Permissions associés à chaque objet monitoring @@ -22,7 +22,14 @@ class GnModuleSchemaConf(Schema): PERMISSION_LEVEL = fields.Dict( keys=fields.Str(), values=fields.Str(), load_default=PERMISSION_LEVEL_DEFAULT ) + DESCRIPTION_MODULE = fields.String(missing="Vous trouverez ici la liste des modules") + TITLE_MODULE = fields.String(missing="Module de suivi") + PERMISSION_LEVEL = fields.Dict( + keys=fields.Str(), values=fields.Str(), load_default=PERMISSION_LEVEL_DEFAULT + ) + DESCRIPTION_MODULE = fields.String(missing="Vous trouverez ici la liste des modules") + TITLE_MODULE = fields.String(missing="Module de suivi") # AREA_TYPE = fields.List(fields.String(), missing=["COM", "M1", "M5", "M10"]) # BORNE_OBS = fields.List(fields.Integer(), missing=[1, 20, 40, 60, 80, 100, 120]) diff --git a/frontend/app/components/modules/modules.component.css b/frontend/app/components/modules/modules.component.css index 5ce13655f..5036d2eb9 100644 --- a/frontend/app/components/modules/modules.component.css +++ b/frontend/app/components/modules/modules.component.css @@ -38,14 +38,14 @@ h2 { } a { - text-decoration:none; - color:initial; + text-decoration: none; + color: initial; } .flex-item:hover { - opacity: 1; - box-shadow: 0px 0px 10px black; - transition: opacity 0.2s, box-shadow 0.2s; + opacity: 1; + box-shadow: 0px 0px 10px black; + transition: opacity 0.2s, box-shadow 0.2s; } .module h2 { @@ -53,17 +53,15 @@ a { } -.module-card:hover -{ - border: 1px solid #303030; - padding: 5px; - color:gray; - transition: 1s ease; +.module-card:hover { + border: 1px solid #303030; + padding: 5px; + color: gray; + transition: 1s ease; } -.module-card -{ - background-color:#71717129; +.module-card { + background-color: #71717129; transition: 1s ease; } diff --git a/frontend/app/components/modules/modules.component.html b/frontend/app/components/modules/modules.component.html index 207e94f5e..3519e09b6 100644 --- a/frontend/app/components/modules/modules.component.html +++ b/frontend/app/components/modules/modules.component.html @@ -1,18 +1,27 @@ -
- -

Chargement en cours

-
- -
-
- -

Modules de suivi

+ +

Chargement en cours

+
+ +
+
- \ No newline at end of file diff --git a/frontend/app/components/modules/modules.component.ts b/frontend/app/components/modules/modules.component.ts index f75d28bb6..a0d411dbe 100644 --- a/frontend/app/components/modules/modules.component.ts +++ b/frontend/app/components/modules/modules.component.ts @@ -3,9 +3,10 @@ import { Component, OnInit } from '@angular/core'; import { mergeMap } from 'rxjs/operators'; /** services */ -import { DataMonitoringObjectService } from '../../services/data-monitoring-object.service'; -import { ConfigService } from '../../services/config.service'; -import { get } from 'https'; +import { DataMonitoringObjectService } from "../../services/data-monitoring-object.service"; +import { ConfigService } from "../../services/config.service"; +import { get } from "https"; +import { AuthService, User } from "@geonature/components/auth/auth.service"; @Component({ selector: 'pnx-monitoring-modules', @@ -13,6 +14,12 @@ import { get } from 'https'; styleUrls: ['./modules.component.css'], }) export class ModulesComponent implements OnInit { + + + currentUser: User; + + description: string; + titleModule: string; modules: Array = []; backendUrl: string; @@ -24,9 +31,10 @@ export class ModulesComponent implements OnInit { bLoading = false; constructor( + private _auth: AuthService, private _dataMonitoringObjectService: DataMonitoringObjectService, private _configService: ConfigService - ) {} + ) { } ngOnInit() { this.bLoading = true; @@ -47,6 +55,19 @@ export class ModulesComponent implements OnInit { this._configService.appConfig.MEDIA_URL }/monitorings/`; this.bLoading = false; + this.description = this._configService.descriptionModule(); + this.titleModule = this._configService.titleModule(); }); + + this.currentUser = this._auth.getCurrentUser(); + + this.currentUser["cruved"] = {}; + this.currentUser["cruved_objects"] = {}; + } + + onAccessSitesClick(modules) { + console.log("accès aux sites avec droits ") + console.log(modules) } + } diff --git a/frontend/app/services/config.service.ts b/frontend/app/services/config.service.ts index 874e86f48..c85ef94fd 100644 --- a/frontend/app/services/config.service.ts +++ b/frontend/app/services/config.service.ts @@ -65,6 +65,12 @@ export class ConfigService { return `${api_url}${this._moduleService.currentModule.module_path}`; } + descriptionModule() { + return ModuleConfig.DESCRIPTION_MODULE; + } + titleModule() { + return ModuleConfig.TITLE_MODULE; + } /** Frontend Module Monitoring Url */ frontendModuleMonitoringUrl() { return this._moduleService.currentModule.module_path; diff --git a/monitorings_config.toml.example b/monitorings_config.toml.example index f04d33639..e77362652 100644 --- a/monitorings_config.toml.example +++ b/monitorings_config.toml.example @@ -1 +1,5 @@ # Fichier listant les paramètres du module et leurs valeurs par défaut + +#Possibilité de rajouter une description au module de suivi +#DESCRIPTION_MODULE = "ceci est une description" +#TITLE_MODULE = "Module de suivi" \ No newline at end of file From 5baf4f7d3f1b1a231a50b52b40fe57df06e41a2f Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Mon, 19 Dec 2022 15:19:57 +0100 Subject: [PATCH 004/161] Feat/monitoring sites (#16) * feat(api): wip began add site routes + tests With site categories Also add tests * feat(api): add more routes * test(api): add tests and fixtures * style(api): applied black * feat(db): add migration to remove id_module Column in t_sites_groups * refactor(api): move utils for routes from sites * feat(api): wip: add sites groups route * test(api): wip: begin adding fixture site_groups * fix: remove id_module in all models * chore: rename route for better consistency * tests: moved site_groups in tests and add tests * chore(api): applied black * refactor(api): add filter params function And refact routes to use it --- ...1f54_remove_id_module_from_sites_groups.py | 54 +++++++++++++++++++ .../gn_module_monitoring/monitoring/models.py | 33 ++++++------ backend/gn_module_monitoring/routes/site.py | 47 ++++++++++++++++ .../routes/sites_groups.py | 18 +++++++ .../gn_module_monitoring/tests/__init__.py | 0 .../gn_module_monitoring/tests/conftest.py | 2 + .../tests/fixtures/site.py | 45 ++++++++++++++++ .../tests/fixtures/sites_groups.py | 16 ++++++ .../tests/test_monitoring/__init__.py | 0 .../test_monitoring/test_routes/__init__.py | 0 .../test_monitoring/test_routes/test_site.py | 41 ++++++++++++++ .../test_routes/test_sites_groups.py | 25 +++++++++ backend/gn_module_monitoring/utils/routes.py | 26 +++++++++ 13 files changed, 289 insertions(+), 18 deletions(-) create mode 100644 backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py create mode 100644 backend/gn_module_monitoring/routes/site.py create mode 100644 backend/gn_module_monitoring/routes/sites_groups.py create mode 100644 backend/gn_module_monitoring/tests/__init__.py create mode 100644 backend/gn_module_monitoring/tests/conftest.py create mode 100644 backend/gn_module_monitoring/tests/fixtures/site.py create mode 100644 backend/gn_module_monitoring/tests/fixtures/sites_groups.py create mode 100644 backend/gn_module_monitoring/tests/test_monitoring/__init__.py create mode 100644 backend/gn_module_monitoring/tests/test_monitoring/test_routes/__init__.py create mode 100644 backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_site.py create mode 100644 backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_sites_groups.py create mode 100644 backend/gn_module_monitoring/utils/routes.py diff --git a/backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py b/backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py new file mode 100644 index 000000000..52c45d8e7 --- /dev/null +++ b/backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py @@ -0,0 +1,54 @@ +"""remove_id_module_from_sites_groups + +Revision ID: f24adb481f54 +Revises: +Create Date: 2022-12-13 16:00:00.512562 + +""" +import sqlalchemy as sa +from alembic import op + +from gn_module_monitoring import MODULE_CODE + +# revision identifiers, used by Alembic. +revision = "f24adb481f54" +down_revision = "b53bafb13ce8" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" + + +def upgrade(): + op.drop_column("t_sites_groups", "id_module", schema=monitorings_schema) + + +def downgrade(): + op.add_column( + "t_sites_groups", + sa.Column( + "id_module", + sa.Integer(), + sa.ForeignKey( + f"gn_commons.t_modules.id_module", + name="fk_t_sites_groups_id_module", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=True, + ), + schema=monitorings_schema, + ) + # Cannot use orm here because need the model to be "downgraded" as well + # Need to set nullable True above for existing rows + # FIXME: find a better way because need to assign a module... + statement = sa.text( + f""" + update {monitorings_schema}.t_sites_groups + set id_module = (select id_module + from gn_commons.t_modules tm + where module_code = '\:module_code'); + """ + ) + op.execute(statement, module_code=MODULE_CODE) + op.alter_column("t_sites_groups", "id_module", nullable=False) diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index 96d11c994..726064370 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -195,10 +195,6 @@ class TMonitoringSitesGroups(DB.Model): id_sites_group = DB.Column(DB.Integer, primary_key=True, nullable=False, unique=True) - id_module = DB.Column( - DB.ForeignKey("gn_commons.t_modules.id_module"), nullable=False, unique=True - ) - uuid_sites_group = DB.Column(UUID(as_uuid=True), default=uuid4) sites_group_name = DB.Column(DB.Unicode) @@ -271,21 +267,22 @@ class TMonitoringModules(TModules): lazy="joined", ) - sites = DB.relationship( - "TMonitoringSites", - uselist=True, # pourquoi pas par defaut ? - primaryjoin=TMonitoringSites.id_module == id_module, - foreign_keys=[id_module], - lazy="select", - ) + # TODO: restore it with CorCategorySite + # sites = DB.relationship( + # 'TMonitoringSites', + # uselist=True, # pourquoi pas par defaut ? + # primaryjoin=TMonitoringSites.id_module == id_module, + # foreign_keys=[id_module], + # lazy="select", + # ) - sites_groups = DB.relationship( - "TMonitoringSitesGroups", - uselist=True, # pourquoi pas par defaut ? - primaryjoin=TMonitoringSitesGroups.id_module == id_module, - foreign_keys=[id_module], - lazy="select", - ) + # sites_groups = DB.relationship( + # 'TMonitoringSitesGroups', + # uselist=True, # pourquoi pas par defaut ? + # primaryjoin=TMonitoringSitesGroups.id_module == id_module, + # foreign_keys=[id_module], + # lazy="select", + # ) datasets = DB.relationship( "TDatasets", diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py new file mode 100644 index 000000000..9adf65bec --- /dev/null +++ b/backend/gn_module_monitoring/routes/site.py @@ -0,0 +1,47 @@ +from typing import Tuple + +from flask import request +from flask.json import jsonify +from geonature.core.gn_monitoring.models import TBaseSites +from werkzeug.datastructures import MultiDict + +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) + + +@blueprint.route("/sites/categories", methods=["GET"]) +def get_categories(): + params = MultiDict(request.args) + limit, page = get_limit_offset(params=params) + + 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) + + +@blueprint.route("/sites/categories/", methods=["GET"]) +def get_categories_by_id(id_categorie): + query = BibCategorieSite.query.filter_by(id_categorie=id_categorie) + res = query.first() + + return jsonify(res.as_dict()) + + +@blueprint.route("/sites", methods=["GET"]) +def get_sites(): + params = MultiDict(request.args) + # TODO: add filter support + limit, page = get_limit_offset(params=params) + query = TBaseSites.query.join( + BibCategorieSite, TBaseSites.id_categorie == BibCategorieSite.id_categorie + ) + query = filter_params(query=query, params=params) + return paginate(query=query, object_name="sites", limit=limit, page=page) + + +@blueprint.route("/sites/module/", methods=["GET"]) +def get_module_sites(module_code: str): + # TODO: load with site_categories.json API + return jsonify({"module_code": module_code}) diff --git a/backend/gn_module_monitoring/routes/sites_groups.py b/backend/gn_module_monitoring/routes/sites_groups.py new file mode 100644 index 000000000..bb3835e5d --- /dev/null +++ b/backend/gn_module_monitoring/routes/sites_groups.py @@ -0,0 +1,18 @@ +from flask import request +from werkzeug.datastructures import MultiDict + +from gn_module_monitoring.blueprint import blueprint +from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups +from gn_module_monitoring.utils.routes import (filter_params, get_limit_offset, + paginate) + + +@blueprint.route("/sites_groups", methods=["GET"]) +def get_sites_groups(): + params = MultiDict(request.args) + limit, page = get_limit_offset(params=params) + + query = filter_params(query=TMonitoringSitesGroups.query, params=params) + + query = query.order_by(TMonitoringSitesGroups.id_sites_group) + return paginate(query=query, object_name="sites_groups", limit=limit, page=page) diff --git a/backend/gn_module_monitoring/tests/__init__.py b/backend/gn_module_monitoring/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/conftest.py b/backend/gn_module_monitoring/tests/conftest.py new file mode 100644 index 000000000..b0a007542 --- /dev/null +++ b/backend/gn_module_monitoring/tests/conftest.py @@ -0,0 +1,2 @@ +from geonature.tests.fixtures import * +from geonature.tests.fixtures import _session, app, users diff --git a/backend/gn_module_monitoring/tests/fixtures/site.py b/backend/gn_module_monitoring/tests/fixtures/site.py new file mode 100644 index 000000000..a74b4d22c --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/site.py @@ -0,0 +1,45 @@ +import pytest +from geoalchemy2.shape import from_shape +from geonature.core.gn_monitoring.models import TBaseSites +from geonature.utils.env import db +from pypnnomenclature.models import BibNomenclaturesTypes, TNomenclatures +from shapely.geometry import Point + +from gn_module_monitoring.monitoring.models import BibCategorieSite + + +@pytest.fixture() +def categories(): + categories = [{"label": "gite", "config": {}}, {"label": "eolienne", "config": {}}] + + categories = {cat["label"]: BibCategorieSite(**cat) for cat in categories} + + with db.session.begin_nested(): + db.session.add_all(categories.values()) + + return categories + + +@pytest.fixture() +def sites(users, categories): + user = users["user"] + geom_4326 = from_shape(Point(43, 24), srid=4326) + sites = {} + # TODO: get_nomenclature from label + site_type = TNomenclatures.query.filter( + BibNomenclaturesTypes.mnemonique == "TYPE_SITE", TNomenclatures.mnemonique == "Grotte" + ).one() + for i, key in enumerate(categories.keys()): + sites[key] = TBaseSites( + id_inventor=user.id_role, + id_digitiser=user.id_role, + base_site_name=f"Site{i}", + base_site_description=f"Description{i}", + base_site_code=f"Code{i}", + geom=geom_4326, + id_nomenclature_type_site=site_type.id_nomenclature, + id_categorie=categories[key].id_categorie, + ) + with db.session.begin_nested(): + db.session.add_all(sites.values()) + return sites diff --git a/backend/gn_module_monitoring/tests/fixtures/sites_groups.py b/backend/gn_module_monitoring/tests/fixtures/sites_groups.py new file mode 100644 index 000000000..bfadf071e --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/sites_groups.py @@ -0,0 +1,16 @@ +import pytest +from geonature.utils.env import db + +from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups + + +@pytest.fixture +def sites_groups(): + names = ["Site_eolien", "Site_Groupe"] + + groups = {name: TMonitoringSitesGroups(sites_group_name=name) for name in names} + + with db.session.begin_nested(): + db.session.add_all(groups.values()) + + return groups diff --git a/backend/gn_module_monitoring/tests/test_monitoring/__init__.py b/backend/gn_module_monitoring/tests/test_monitoring/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_routes/__init__.py b/backend/gn_module_monitoring/tests/test_monitoring/test_routes/__init__.py new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..56deccbd5 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_site.py @@ -0,0 +1,41 @@ +import pytest +from flask import url_for + +from gn_module_monitoring.tests.fixtures.site import categories, sites + + +@pytest.mark.usefixtures("client_class", "temporary_transaction") +class TestSite: + def test_get_categories_by_id(self, categories): + for cat in categories.values(): + r = self.client.get( + url_for( + "monitorings.get_categories_by_id", + id_categorie=cat.id_categorie, + ) + ) + assert r.json["label"] == cat.label + + 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()]) + + 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"] + + def test_get_sites(self, sites): + r = self.client.get(url_for("monitorings.get_sites")) + + assert r.json["count"] >= len(sites) + assert any([site.as_dict() in r.json["sites"] for site in sites.values()]) + + def test_get_module_sites(self): + module_code = "TEST" + r = self.client.get(url_for("monitorings.get_module_sites", module_code=module_code)) + + assert r.json["module_code"] == module_code diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_sites_groups.py b/backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_sites_groups.py new file mode 100644 index 000000000..eef8a335e --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_sites_groups.py @@ -0,0 +1,25 @@ +import pytest +from flask import url_for + +from gn_module_monitoring.tests.fixtures.sites_groups import sites_groups + + +@pytest.mark.usefixtures("client_class", "temporary_transaction") +class TestSitesGroups: + def test_get_sites_groups(self, sites_groups): + r = self.client.get(url_for("monitorings.get_sites_groups")) + + assert r.json["count"] >= len(sites_groups) + assert all([group.as_dict() in r.json["sites_groups"] for group in sites_groups.values()]) + + def test_get_sites_groups_filter_name(self, sites_groups): + name, name_not_present = list(sites_groups.keys()) + + r = self.client.get( + url_for("monitorings.get_sites_groups"), query_string={"sites_group_name": name} + ) + + assert r.json["count"] >= 1 + json_sites_groups = r.json["sites_groups"] + assert sites_groups[name].as_dict() in json_sites_groups + assert sites_groups[name_not_present].as_dict() not in json_sites_groups diff --git a/backend/gn_module_monitoring/utils/routes.py b/backend/gn_module_monitoring/utils/routes.py new file mode 100644 index 000000000..e2b0fa48c --- /dev/null +++ b/backend/gn_module_monitoring/utils/routes.py @@ -0,0 +1,26 @@ +from typing import Tuple + +from flask import Response +from flask.json import jsonify +from sqlalchemy.orm import Query +from werkzeug.datastructures import MultiDict + + +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: + result = query.paginate(page=page, error_out=False, max_per_page=limit) + data = { + object_name: [res.as_dict() for res in result.items], + "count": result.total, + "limit": limit, + "offset": page - 1, + } + return jsonify(data) + +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 From 95c7b180d21fd74615aa2ff1521cb861a648457b 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 005/161] 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 | 31 +++++++++++- 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, 208 insertions(+), 16 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 726064370..44637f00c 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 TMonitoringObservationDetails(DB.Model): 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 From 94ef8f935b5ec40409591e46ea032da602cc58a5 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Thu, 22 Dec 2022 14:27:07 +0100 Subject: [PATCH 006/161] Feat/edit categories module (#19) * tests: make tests and fixtures work for modules By changing the way a monitoring module is created in the fixture Add two tests to check the relationship * feat(api): add categories in edit module * style: applied black --- .../config/generic/module.json | 15 ++++++++++++ .../gn_module_monitoring/routes/data_utils.py | 4 ++-- .../tests/fixtures/module.py | 23 +++++++++++-------- .../test_models/test_module.py | 18 +++++++++++---- 4 files changed, 44 insertions(+), 16 deletions(-) diff --git a/backend/gn_module_monitoring/config/generic/module.json b/backend/gn_module_monitoring/config/generic/module.json index 352a5dacf..e270a49d9 100644 --- a/backend/gn_module_monitoring/config/generic/module.json +++ b/backend/gn_module_monitoring/config/generic/module.json @@ -120,6 +120,21 @@ "attribut_label": "Afficher dans le menu ?", "definition": "Afficher le module dans le menu de GeoNature. (Recharger la page pour voir les modifications)." }, + + "categories": { + "type_widget": "datalist", + "attribut_label": "Liste des catégories", + "type_util": "categorie", + "keyValue": "id_categorie", + "keyLabel": "label", + "multiple": true, + "api" : "__MONITORINGS_PATH/sites/categories", + "application": "GeoNature", + "required": true, + "data_path": "categories", + "definition": "Permet de paramétrer la compatibilité de ce module avec les catégories de sites" + }, + "medias": { "type_widget": "medias", "attribut_label": "Médias", diff --git a/backend/gn_module_monitoring/routes/data_utils.py b/backend/gn_module_monitoring/routes/data_utils.py index 2c0fae73b..2038f5f34 100644 --- a/backend/gn_module_monitoring/routes/data_utils.py +++ b/backend/gn_module_monitoring/routes/data_utils.py @@ -32,8 +32,7 @@ from ..blueprint import blueprint from ..config.repositories import get_config - -from ..monitoring.models import TMonitoringSitesGroups, TMonitoringSites +from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups, TMonitoringSites, BibCategorieSite model_dict = { "habitat": Habref, @@ -41,6 +40,7 @@ "user": User, "taxonomy": Taxref, "dataset": TDatasets, + "categorie": BibCategorieSite, "observer_list": UserList, "taxonomy_list": BibListes, "sites_group": TMonitoringSitesGroups, diff --git a/backend/gn_module_monitoring/tests/fixtures/module.py b/backend/gn_module_monitoring/tests/fixtures/module.py index 6e0a5b1ac..41d946e37 100644 --- a/backend/gn_module_monitoring/tests/fixtures/module.py +++ b/backend/gn_module_monitoring/tests/fixtures/module.py @@ -1,5 +1,6 @@ +from uuid import uuid4 + 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 @@ -7,15 +8,17 @@ @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() +def monitoring_module(categories): + t_monitoring_module = TMonitoringModules( + module_code=uuid4(), + module_label="test", + active_frontend=True, + active_backend=False, + module_path="test", + categories=list(categories.values()), + ) - 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) + db.session.add(t_monitoring_module) - return monitoring + return t_monitoring_module 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 index e156a1614..60cca2e98 100644 --- 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 @@ -1,11 +1,21 @@ import pytest +from geonature.utils.env import db +from gn_module_monitoring.monitoring.models import TMonitoringModules from gn_module_monitoring.tests.fixtures.module import monitoring_module -from gn_module_monitoring.tests.fixtures.site import categories +from gn_module_monitoring.tests.fixtures.site import categories, site_type @pytest.mark.usefixtures("temporary_transaction") class TestModule: - def test_module(self, monitoring_module): - cateogories = monitoring_module.categories - assert False + def test_module(self, monitoring_module, categories): + cats = monitoring_module.categories + assert cats == list(categories.values()) + + def test_remove_categorie_from_module(self, monitoring_module, categories): + with db.session.begin_nested(): + monitoring_module.categories.pop(0) + + mon = TMonitoringModules.query.filter_by(id_module=monitoring_module.id_module).one() + + assert len(mon.categories) == len(categories) - 1 From e963eac74708e1bb110f866b8302d6b958ad2e2d Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Fri, 23 Dec 2022 10:28:17 +0100 Subject: [PATCH 007/161] test: refactor fixtures to load them automatically (#20) Need to set the current working directory of pytest the directory where the gn_monitoring_module repo is (so with the frontend etc.) --- backend/gn_module_monitoring/tests/conftest.py | 6 ++++++ .../tests/test_monitoring/test_models/test_module.py | 2 -- .../tests/test_monitoring/test_routes/test_site.py | 2 -- .../tests/test_monitoring/test_routes/test_sites_groups.py | 2 -- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/gn_module_monitoring/tests/conftest.py b/backend/gn_module_monitoring/tests/conftest.py index b0a007542..6b1107a1d 100644 --- a/backend/gn_module_monitoring/tests/conftest.py +++ b/backend/gn_module_monitoring/tests/conftest.py @@ -1,2 +1,8 @@ from geonature.tests.fixtures import * from geonature.tests.fixtures import _session, app, users + +pytest_plugins = [ + "gn_module_monitoring.tests.fixtures.module", + "gn_module_monitoring.tests.fixtures.site", + "gn_module_monitoring.tests.fixtures.sites_groups", +] 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 index 60cca2e98..fbf606816 100644 --- 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 @@ -2,8 +2,6 @@ from geonature.utils.env import db from gn_module_monitoring.monitoring.models import TMonitoringModules -from gn_module_monitoring.tests.fixtures.module import monitoring_module -from gn_module_monitoring.tests.fixtures.site import categories, site_type @pytest.mark.usefixtures("temporary_transaction") 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 9c7f8ac16..04dc22995 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,8 +1,6 @@ import pytest from flask import url_for -from gn_module_monitoring.tests.fixtures.site import categories, site_type, sites - @pytest.mark.usefixtures("client_class", "temporary_transaction") class TestSite: diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_sites_groups.py b/backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_sites_groups.py index eef8a335e..41701d18c 100644 --- a/backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_sites_groups.py +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_sites_groups.py @@ -1,8 +1,6 @@ import pytest from flask import url_for -from gn_module_monitoring.tests.fixtures.sites_groups import sites_groups - @pytest.mark.usefixtures("client_class", "temporary_transaction") class TestSitesGroups: From 47d1b7e4fc21a88b90f03aa78e759936bdea58e7 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Fri, 23 Dec 2022 10:56:40 +0100 Subject: [PATCH 008/161] test: move test_route in parent dir (#17) clean: Merge la PR move test_route Co-authored-by: Andria Capai --- .../tests/{test_monitoring => }/test_routes/__init__.py | 0 .../tests/{test_monitoring => }/test_routes/test_site.py | 0 .../tests/{test_monitoring => }/test_routes/test_sites_groups.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename backend/gn_module_monitoring/tests/{test_monitoring => }/test_routes/__init__.py (100%) rename backend/gn_module_monitoring/tests/{test_monitoring => }/test_routes/test_site.py (100%) rename backend/gn_module_monitoring/tests/{test_monitoring => }/test_routes/test_sites_groups.py (100%) diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_routes/__init__.py b/backend/gn_module_monitoring/tests/test_routes/__init__.py similarity index 100% rename from backend/gn_module_monitoring/tests/test_monitoring/test_routes/__init__.py rename to backend/gn_module_monitoring/tests/test_routes/__init__.py diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_site.py b/backend/gn_module_monitoring/tests/test_routes/test_site.py similarity index 100% rename from backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_site.py rename to backend/gn_module_monitoring/tests/test_routes/test_site.py diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_sites_groups.py b/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py similarity index 100% rename from backend/gn_module_monitoring/tests/test_monitoring/test_routes/test_sites_groups.py rename to backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py From dbf4571f3d35f271949cb85754f7a26735a0a552 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Wed, 4 Jan 2023 11:37:39 +0100 Subject: [PATCH 009/161] Feat/create marshmallow schemas and remove id_module (#21) * feat(api): remove id_module from sites_complements Co-authored-by: andriacap * feat(api): create schema for sites_groups * test: add sites_group to site fixture * test: wip add test for sites_groups schemas * chore(api): remove depth parameter from paginate * test: updated to work with sites_group schema * feat: categorie site with marshmallow Test and marshmallow create/refactor to adapt for bibcategorie site paginate WIP : Adapt load_only site_type in test to "assert" same object when initiate BibCategorieSite [Refs ticket]: #3 * feat(api): route /sites/categories/id with schema Changing the route to return a dump Marshmallow schema BibCategorieSitesSchema Reviewed-by: andriac [Refs ticket]: #3 * test(api): routes get categoires label Change the "as_dict" by schema.dump in order to use the Marshmallow schema created Reviewed-by: andriac [Refs ticket]: #3 * feat(api): Sites: cols to geoserializable + schema * style(api): applied black * test(api): add test for Site Schema * style(api): applied black to test_site * refactor(api): instantiate schema once Instead of for each all() iteration * chore(api): remove useless comments * chore(api): remove useless comments and imports Co-authored-by: andriacap Co-authored-by: Andria Capai --- ...remove_id_module_from_sites_complements.py | 54 ++++++++++++++++++ .../gn_module_monitoring/monitoring/models.py | 6 +- .../monitoring/schemas.py | 57 +++++++++++++++++++ backend/gn_module_monitoring/routes/site.py | 19 +++++-- .../routes/sites_groups.py | 11 +++- .../tests/fixtures/site.py | 13 +++-- .../tests/test_routes/test_site.py | 16 ++++-- .../tests/test_routes/test_sites_groups.py | 26 +++++++-- backend/gn_module_monitoring/utils/routes.py | 15 ++--- 9 files changed, 184 insertions(+), 33 deletions(-) create mode 100644 backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py create mode 100644 backend/gn_module_monitoring/monitoring/schemas.py diff --git a/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py b/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py new file mode 100644 index 000000000..ca6d04879 --- /dev/null +++ b/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py @@ -0,0 +1,54 @@ +"""remove_id_module_from_sites_complements + +Revision ID: 6673266fb79c +Revises: +Create Date: 2022-12-13 16:00:00.512562 + +""" +import sqlalchemy as sa +from alembic import op + +from gn_module_monitoring import MODULE_CODE + +# revision identifiers, used by Alembic. +revision = "6673266fb79c" +down_revision = "e64bafb13ce8" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" + + +def upgrade(): + op.drop_column("t_site_complements", "id_module", schema=monitorings_schema) + + +def downgrade(): + op.add_column( + "t_site_complements", + sa.Column( + "id_module", + sa.Integer(), + sa.ForeignKey( + f"gn_commons.t_modules.id_module", + name="fk_t_site_complements_id_module", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=True, + ), + schema=monitorings_schema, + ) + # Cannot use orm here because need the model to be "downgraded" as well + # Need to set nullable True above for existing rows + # FIXME: find a better way because need to assign a module... + statement = sa.text( + f""" + update {monitorings_schema}.t_site_complements + set id_module = (select id_module + from gn_commons.t_modules tm + where module_code = :module_code); + """ + ).bindparams(module_code=MODULE_CODE) + op.execute(statement) + op.alter_column("t_site_complements", "id_module", nullable=False, schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index 44637f00c..2602a7be8 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -155,7 +155,7 @@ class TMonitoringVisits(TBaseVisits): ) -@geoserializable +@geoserializable(geoCol="geom", idCol="id_base_site") class TMonitoringSites(TBaseSites): __tablename__ = "t_site_complements" __table_args__ = {"schema": "gn_monitoring"} @@ -167,10 +167,6 @@ class TMonitoringSites(TBaseSites): DB.ForeignKey("gn_monitoring.t_base_sites.id_base_site"), nullable=False, primary_key=True ) - id_module = DB.Column( - DB.ForeignKey("gn_commons.t_modules.id_module"), - nullable=False, - ) id_sites_group = DB.Column( DB.ForeignKey( diff --git a/backend/gn_module_monitoring/monitoring/schemas.py b/backend/gn_module_monitoring/monitoring/schemas.py new file mode 100644 index 000000000..2721de0e8 --- /dev/null +++ b/backend/gn_module_monitoring/monitoring/schemas.py @@ -0,0 +1,57 @@ +import json + +import geojson +from marshmallow import Schema, fields +from marshmallow_sqlalchemy import SQLAlchemyAutoSchema +from pypnnomenclature.schemas import NomenclatureSchema + +from gn_module_monitoring.monitoring.models import ( + BibCategorieSite, + TMonitoringSites, + TMonitoringSitesGroups, +) + + +def paginate_schema(schema): + class PaginationSchema(Schema): + count = fields.Integer() + limit = fields.Integer() + offset = fields.Integer() + items = fields.Nested(schema, many=True, dump_only=True) + + return PaginationSchema + + +class MonitoringSitesGroupsSchema(SQLAlchemyAutoSchema): + class Meta: + model = TMonitoringSitesGroups + exclude = ("geom_geojson",) + + geometry = fields.Method("serialize_geojson", dump_only=True) + + def serialize_geojson(self, obj): + if obj.geom_geojson is not None: + return json.loads(obj.geom_geojson) + + +class MonitoringSitesSchema(SQLAlchemyAutoSchema): + class Meta: + model = TMonitoringSites + exclude = ("geom_geojson", "geom") + + geometry = fields.Method("serialize_geojson", dump_only=True) + + def serialize_geojson(self, obj): + if obj.geom is not None: + return geojson.dumps(obj.as_geofeature().get("geometry")) + + +class BibCategorieSiteSchema(SQLAlchemyAutoSchema): + site_type = fields.Nested( + NomenclatureSchema(only=("id_nomenclature", "label_fr")), many=True, dump_only=True + ) + + class Meta: + model = BibCategorieSite + include_fk = True + load_instance = True diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py index b55d2a07e..cb10c21ca 100644 --- a/backend/gn_module_monitoring/routes/site.py +++ b/backend/gn_module_monitoring/routes/site.py @@ -5,6 +5,7 @@ from gn_module_monitoring.blueprint import blueprint from gn_module_monitoring.monitoring.models import BibCategorieSite +from gn_module_monitoring.monitoring.schemas import MonitoringSitesSchema,BibCategorieSiteSchema from gn_module_monitoring.utils.routes import filter_params, get_limit_offset, paginate @@ -16,15 +17,20 @@ 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, depth=1) + return paginate( + query=query, + schema=BibCategorieSiteSchema, + limit=limit, + page=page, + ) @blueprint.route("/sites/categories/", methods=["GET"]) def get_categories_by_id(id_categorie): query = BibCategorieSite.query.filter_by(id_categorie=id_categorie) res = query.first() - - return jsonify(res.as_dict()) + schema = BibCategorieSiteSchema() + return schema.dump(res) @blueprint.route("/sites", methods=["GET"]) @@ -36,7 +42,12 @@ def get_sites(): BibCategorieSite, TBaseSites.id_categorie == BibCategorieSite.id_categorie ) query = filter_params(query=query, params=params) - return paginate(query=query, object_name="sites", limit=limit, page=page) + return paginate( + query=query, + schema=MonitoringSitesSchema, + limit=limit, + page=page, + ) @blueprint.route("/sites/module/", methods=["GET"]) diff --git a/backend/gn_module_monitoring/routes/sites_groups.py b/backend/gn_module_monitoring/routes/sites_groups.py index bb3835e5d..7ea5609df 100644 --- a/backend/gn_module_monitoring/routes/sites_groups.py +++ b/backend/gn_module_monitoring/routes/sites_groups.py @@ -3,8 +3,8 @@ from gn_module_monitoring.blueprint import blueprint from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups -from gn_module_monitoring.utils.routes import (filter_params, get_limit_offset, - paginate) +from gn_module_monitoring.monitoring.schemas import MonitoringSitesGroupsSchema +from gn_module_monitoring.utils.routes import filter_params, get_limit_offset, paginate @blueprint.route("/sites_groups", methods=["GET"]) @@ -15,4 +15,9 @@ def get_sites_groups(): query = filter_params(query=TMonitoringSitesGroups.query, params=params) query = query.order_by(TMonitoringSitesGroups.id_sites_group) - return paginate(query=query, object_name="sites_groups", limit=limit, page=page) + return paginate( + query=query, + schema=MonitoringSitesGroupsSchema, + limit=limit, + page=page, + ) diff --git a/backend/gn_module_monitoring/tests/fixtures/site.py b/backend/gn_module_monitoring/tests/fixtures/site.py index 4325c0bf5..415ffe321 100644 --- a/backend/gn_module_monitoring/tests/fixtures/site.py +++ b/backend/gn_module_monitoring/tests/fixtures/site.py @@ -1,18 +1,18 @@ import pytest from geoalchemy2.shape import from_shape -from geonature.core.gn_monitoring.models import TBaseSites from geonature.utils.env import db from pypnnomenclature.models import BibNomenclaturesTypes, TNomenclatures from shapely.geometry import Point -from gn_module_monitoring.monitoring.models import BibCategorieSite +from gn_module_monitoring.monitoring.models import BibCategorieSite, TMonitoringSites +from gn_module_monitoring.tests.fixtures.sites_groups import sites_groups @pytest.fixture() def site_type(): return TNomenclatures.query.filter( - BibNomenclaturesTypes.mnemonique == "TYPE_SITE", TNomenclatures.mnemonique == "Grotte" - ).one() + BibNomenclaturesTypes.mnemonique == "TYPE_SITE", TNomenclatures.mnemonique == "Grotte" + ).one() @pytest.fixture() @@ -31,7 +31,7 @@ def categories(site_type): @pytest.fixture() -def sites(users, categories): +def sites(users, categories, sites_groups): user = users["user"] geom_4326 = from_shape(Point(43, 24), srid=4326) sites = {} @@ -40,7 +40,7 @@ def sites(users, categories): BibNomenclaturesTypes.mnemonique == "TYPE_SITE", TNomenclatures.mnemonique == "Grotte" ).one() for i, key in enumerate(categories.keys()): - sites[key] = TBaseSites( + sites[key] = TMonitoringSites( id_inventor=user.id_role, id_digitiser=user.id_role, base_site_name=f"Site{i}", @@ -49,6 +49,7 @@ def sites(users, categories): geom=geom_4326, id_nomenclature_type_site=site_type.id_nomenclature, id_categorie=categories[key].id_categorie, + id_sites_group=sites_groups["Site_Groupe"].id_sites_group, ) with db.session.begin_nested(): db.session.add_all(sites.values()) diff --git a/backend/gn_module_monitoring/tests/test_routes/test_site.py b/backend/gn_module_monitoring/tests/test_routes/test_site.py index 04dc22995..6bddc90ca 100644 --- a/backend/gn_module_monitoring/tests/test_routes/test_site.py +++ b/backend/gn_module_monitoring/tests/test_routes/test_site.py @@ -1,6 +1,8 @@ import pytest from flask import url_for +from gn_module_monitoring.monitoring.schemas import BibCategorieSiteSchema, MonitoringSitesSchema + @pytest.mark.usefixtures("client_class", "temporary_transaction") class TestSite: @@ -15,22 +17,28 @@ def test_get_categories_by_id(self, categories): assert r.json["label"] == cat.label def test_get_categories(self, categories): + schema = BibCategorieSiteSchema() + r = self.client.get(url_for("monitorings.get_categories")) assert r.json["count"] >= len(categories) - assert all([cat.as_dict(depth=1) in r.json["categories"] for cat in categories.values()]) + assert all( + [schema.dump(cat) in r.json["items"] for cat in categories.values()] + ) def test_get_categories_label(self, categories): label = list(categories.keys())[0] - + schema = BibCategorieSiteSchema() r = self.client.get(url_for("monitorings.get_categories"), query_string={"label": label}) - assert categories[label].as_dict(depth=1) in r.json["categories"] + assert schema.dump(categories[label]) in r.json["items"] def test_get_sites(self, sites): + schema = MonitoringSitesSchema() + r = self.client.get(url_for("monitorings.get_sites")) assert r.json["count"] >= len(sites) - assert any([site.as_dict() in r.json["sites"] for site in sites.values()]) + assert any([schema.dump(site) in r.json["items"] for site in sites.values()]) def test_get_module_sites(self): module_code = "TEST" diff --git a/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py b/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py index 41701d18c..a81abf66d 100644 --- a/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py +++ b/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py @@ -1,6 +1,9 @@ import pytest from flask import url_for +from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups +from gn_module_monitoring.monitoring.schemas import MonitoringSitesGroupsSchema + @pytest.mark.usefixtures("client_class", "temporary_transaction") class TestSitesGroups: @@ -8,16 +11,31 @@ def test_get_sites_groups(self, sites_groups): r = self.client.get(url_for("monitorings.get_sites_groups")) assert r.json["count"] >= len(sites_groups) - assert all([group.as_dict() in r.json["sites_groups"] for group in sites_groups.values()]) + assert all( + [ + MonitoringSitesGroupsSchema().dump(group) in r.json["items"] + for group in sites_groups.values() + ] + ) def test_get_sites_groups_filter_name(self, sites_groups): name, name_not_present = list(sites_groups.keys()) + schema = MonitoringSitesGroupsSchema() r = self.client.get( url_for("monitorings.get_sites_groups"), query_string={"sites_group_name": name} ) assert r.json["count"] >= 1 - json_sites_groups = r.json["sites_groups"] - assert sites_groups[name].as_dict() in json_sites_groups - assert sites_groups[name_not_present].as_dict() not in json_sites_groups + json_sites_groups = r.json["items"] + assert schema.dump(sites_groups[name]) in json_sites_groups + assert schema.dump(sites_groups[name_not_present]) not in json_sites_groups + + def test_serialize_sites_groups(self, sites_groups, sites): + groups = TMonitoringSitesGroups.query.filter( + TMonitoringSitesGroups.id_sites_group.in_( + [s.id_sites_group for s in sites_groups.values()] + ) + ).all() + schema = MonitoringSitesGroupsSchema() + assert [schema.dump(site) for site in groups] diff --git a/backend/gn_module_monitoring/utils/routes.py b/backend/gn_module_monitoring/utils/routes.py index b6c16a1d7..5a75c1c4b 100644 --- a/backend/gn_module_monitoring/utils/routes.py +++ b/backend/gn_module_monitoring/utils/routes.py @@ -2,22 +2,23 @@ from flask import Response from flask.json import jsonify +from marshmallow import Schema from sqlalchemy.orm import Query from werkzeug.datastructures import MultiDict +from gn_module_monitoring.monitoring.schemas import paginate_schema + 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, depth: int = 0) -> Response: +def paginate(query: Query, schema: Schema, limit: int, page: int) -> Response: result = query.paginate(page=page, error_out=False, max_per_page=limit) - data = { - object_name: [res.as_dict(depth=depth) for res in result.items], - "count": result.total, - "limit": limit, - "offset": page - 1, - } + pagination_schema = paginate_schema(schema) + data = pagination_schema().dump( + dict(items=result.items, count=result.total, limit=limit, offset=page - 1) + ) return jsonify(data) From 7a1d9f05716712041cb77a501fd15c3e3b0eaf2c Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Wed, 4 Jan 2023 11:38:54 +0100 Subject: [PATCH 010/161] refactor(api): remove id_type in admin (#22) To replace it with a bib nomenclature type: TYPE_SITE --- backend/gn_module_monitoring/monitoring/admin.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/gn_module_monitoring/monitoring/admin.py b/backend/gn_module_monitoring/monitoring/admin.py index e9f2559a4..e7184b8ce 100644 --- a/backend/gn_module_monitoring/monitoring/admin.py +++ b/backend/gn_module_monitoring/monitoring/admin.py @@ -1,11 +1,14 @@ 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 pypnnomenclature.models import TNomenclatures, BibNomenclaturesTypes from gn_module_monitoring.monitoring.models import BibCategorieSite +SITE_TYPE = "TYPE_SITE" + + class BibCategorieSiteView(CruvedProtectedMixin, ModelView): """ Surcharge de l'administration des catégories de sites @@ -21,7 +24,8 @@ def __init__(self, session, **kwargs): def get_only_type_site_asc(): return ( DB.session.query(TNomenclatures) - .filter(TNomenclatures.id_type == 116) + .join(TNomenclatures.nomenclature_type) + .filter(BibNomenclaturesTypes.mnemonique == SITE_TYPE) .order_by(TNomenclatures.label_fr.asc()) ) From df02c692d573425d343e909cb752a557a4dd9dff Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Wed, 4 Jan 2023 11:39:21 +0100 Subject: [PATCH 011/161] style(config): rename attribut label of categories (#23) --- backend/gn_module_monitoring/config/generic/module.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/gn_module_monitoring/config/generic/module.json b/backend/gn_module_monitoring/config/generic/module.json index e270a49d9..b84bf5f6b 100644 --- a/backend/gn_module_monitoring/config/generic/module.json +++ b/backend/gn_module_monitoring/config/generic/module.json @@ -123,7 +123,7 @@ "categories": { "type_widget": "datalist", - "attribut_label": "Liste des catégories", + "attribut_label": "Catégories de sites", "type_util": "categorie", "keyValue": "id_categorie", "keyLabel": "label", From 7421fab1b78de12287a4972210d1a451fdc27b99 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Wed, 4 Jan 2023 12:09:23 +0100 Subject: [PATCH 012/161] Fix/paginate utils (#24) * fix(api): add int conversion for limit/offset * test(api): add test for get_limit_offset * fix(api): max_per_page => per_page & test * test(api): update test with changes on schema Since marshmallow schemas, the json returned by pagination has changed for items and not sites --- .../tests/test_routes/test_site.py | 7 +++++++ .../tests/test_utils/__init__.py | 0 .../tests/test_utils/test_routes.py | 14 ++++++++++++++ backend/gn_module_monitoring/utils/routes.py | 4 ++-- 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 backend/gn_module_monitoring/tests/test_utils/__init__.py create mode 100644 backend/gn_module_monitoring/tests/test_utils/test_routes.py diff --git a/backend/gn_module_monitoring/tests/test_routes/test_site.py b/backend/gn_module_monitoring/tests/test_routes/test_site.py index 6bddc90ca..d63591ca5 100644 --- a/backend/gn_module_monitoring/tests/test_routes/test_site.py +++ b/backend/gn_module_monitoring/tests/test_routes/test_site.py @@ -40,6 +40,13 @@ def test_get_sites(self, sites): assert r.json["count"] >= len(sites) assert any([schema.dump(site) in r.json["items"] for site in sites.values()]) + def test_get_sites_limit(self, sites): + limit = 34 + + r = self.client.get(url_for("monitorings.get_sites", limit=limit)) + + assert len(r.json["items"]) == limit + def test_get_module_sites(self): module_code = "TEST" r = self.client.get(url_for("monitorings.get_module_sites", module_code=module_code)) diff --git a/backend/gn_module_monitoring/tests/test_utils/__init__.py b/backend/gn_module_monitoring/tests/test_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/test_utils/test_routes.py b/backend/gn_module_monitoring/tests/test_utils/test_routes.py new file mode 100644 index 000000000..cc6357f37 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_utils/test_routes.py @@ -0,0 +1,14 @@ +import pytest +from werkzeug.datastructures import MultiDict + +from gn_module_monitoring.utils.routes import get_limit_offset + + +@pytest.mark.parametrize("limit, offset", [("1", "2"), (1, 2), ("1", 2), (1, "2")]) +def test_get_limit_offset(limit, offset): + multi_dict = MultiDict([("limit", limit), ("offset", offset)]) + + comp_limit, comp_offset = get_limit_offset(params=multi_dict) + + assert isinstance(comp_limit, int) + assert isinstance(comp_offset, int) diff --git a/backend/gn_module_monitoring/utils/routes.py b/backend/gn_module_monitoring/utils/routes.py index 5a75c1c4b..a7889dc68 100644 --- a/backend/gn_module_monitoring/utils/routes.py +++ b/backend/gn_module_monitoring/utils/routes.py @@ -10,11 +10,11 @@ def get_limit_offset(params: MultiDict) -> Tuple[int]: - return params.pop("limit", 50), params.pop("offset", 1) + return int(params.pop("limit", 50)), int(params.pop("offset", 1)) def paginate(query: Query, schema: Schema, limit: int, page: int) -> Response: - result = query.paginate(page=page, error_out=False, max_per_page=limit) + result = query.paginate(page=page, error_out=False, per_page=limit) pagination_schema = paginate_schema(schema) data = pagination_schema().dump( dict(items=result.items, count=result.total, limit=limit, offset=page - 1) From 0b25ef820ace83f08eccaaafcbd3d13bf2b8d7f5 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Wed, 4 Jan 2023 12:39:04 +0100 Subject: [PATCH 013/161] Feat/improve filter (#25) * feat(api): add Query Class to sites, grps & cats * feat(api): add sort and fix _get_model Via _get_entity * test(api): test sort query * feat(api): add api sort/sort_dir params To be able to sort through REST Api * fix(api): check if integer to avoid using ilike In filter_by_params * test(api): add test to check filter integer --- .../gn_module_monitoring/monitoring/models.py | 8 +++- .../monitoring/queries.py | 37 +++++++++++++++++++ backend/gn_module_monitoring/routes/site.py | 23 +++++++++--- .../routes/sites_groups.py | 14 +++++-- .../test_models/test_sites_groups.py | 36 ++++++++++++++++++ .../tests/test_routes/test_site.py | 22 +++++++++-- backend/gn_module_monitoring/utils/routes.py | 15 +++++++- 7 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 backend/gn_module_monitoring/monitoring/queries.py create mode 100644 backend/gn_module_monitoring/tests/test_monitoring/test_models/test_sites_groups.py diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index 2602a7be8..99d082d7e 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -20,6 +20,8 @@ from pypnusershub.db.models import User from geonature.core.gn_monitoring.models import corVisitObserver +from gn_module_monitoring.monitoring.queries import Query as MonitoringQuery + cor_module_categorie = DB.Table( "cor_module_categorie", DB.Column( @@ -162,6 +164,7 @@ class TMonitoringSites(TBaseSites): __mapper_args__ = { "polymorphic_identity": "monitoring_site", } + query_class = MonitoringQuery id_base_site = DB.Column( DB.ForeignKey("gn_monitoring.t_base_sites.id_base_site"), nullable=False, primary_key=True @@ -215,8 +218,9 @@ class TMonitoringSites(TBaseSites): @serializable class TMonitoringSitesGroups(DB.Model): - __tablename__ = "t_sites_groups" - __table_args__ = {"schema": "gn_monitoring"} + __tablename__ = 't_sites_groups' + __table_args__ = {'schema': 'gn_monitoring'} + query_class = MonitoringQuery id_sites_group = DB.Column(DB.Integer, primary_key=True, nullable=False, unique=True) diff --git a/backend/gn_module_monitoring/monitoring/queries.py b/backend/gn_module_monitoring/monitoring/queries.py new file mode 100644 index 000000000..cc2d6f972 --- /dev/null +++ b/backend/gn_module_monitoring/monitoring/queries.py @@ -0,0 +1,37 @@ +from flask_sqlalchemy import BaseQuery +from sqlalchemy import Integer, and_ +from werkzeug.datastructures import MultiDict + + +class Query(BaseQuery): + def _get_entity(self, entity): + if hasattr(entity, "_entities"): + return self._get_entity(entity._entities[0]) + return entity.entities[0] + + def _get_model(self): + # When sqlalchemy is updated: + # return self._raw_columns[0].entity_namespace + # But for now: + entity = self._get_entity(self) + return entity.c + + def filter_by_params(self, params: MultiDict = None): + model = self._get_model() + and_list = [] + for key, value in params.items(): + column = getattr(model, key) + if isinstance(column.type, Integer): + and_list.append(column == value) + else: + and_list.append(column.ilike(f"%{value}%")) + and_query = and_(*and_list) + return self.filter(and_query) + + def sort(self, label: str, direction: str): + model = self._get_model() + order_by = getattr(model, label) + if direction == "desc": + order_by = order_by.desc() + + return self.order_by(order_by) diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py index cb10c21ca..1a53ca316 100644 --- a/backend/gn_module_monitoring/routes/site.py +++ b/backend/gn_module_monitoring/routes/site.py @@ -4,18 +4,27 @@ from werkzeug.datastructures import MultiDict from gn_module_monitoring.blueprint import blueprint -from gn_module_monitoring.monitoring.models import BibCategorieSite +from gn_module_monitoring.monitoring.models import BibCategorieSite, TMonitoringSites +from gn_module_monitoring.utils.routes import ( + filter_params, + get_limit_offset, + get_sort, + paginate, + sort, +) from gn_module_monitoring.monitoring.schemas import MonitoringSitesSchema,BibCategorieSiteSchema -from gn_module_monitoring.utils.routes import filter_params, get_limit_offset, paginate @blueprint.route("/sites/categories", methods=["GET"]) def get_categories(): params = MultiDict(request.args) limit, page = get_limit_offset(params=params) + sort_label, sort_dir = get_sort( + params=params, default_sort="id_categorie", default_direction="desc" + ) query = filter_params(query=BibCategorieSite.query, params=params) - query = query.order_by(BibCategorieSite.id_categorie) + query = sort(query=query, sort=sort_label, sort_dir=sort_dir) return paginate( query=query, @@ -38,10 +47,14 @@ def get_sites(): params = MultiDict(request.args) # TODO: add filter support limit, page = get_limit_offset(params=params) - query = TBaseSites.query.join( - BibCategorieSite, TBaseSites.id_categorie == BibCategorieSite.id_categorie + sort_label, sort_dir = get_sort( + params=params, default_sort="id_base_site", default_direction="desc" + ) + query = TMonitoringSites.query.join( + BibCategorieSite, TMonitoringSites.id_categorie == BibCategorieSite.id_categorie ) query = filter_params(query=query, params=params) + query = sort(query=query, sort=sort_label, sort_dir=sort_dir) return paginate( query=query, schema=MonitoringSitesSchema, diff --git a/backend/gn_module_monitoring/routes/sites_groups.py b/backend/gn_module_monitoring/routes/sites_groups.py index 7ea5609df..57317445d 100644 --- a/backend/gn_module_monitoring/routes/sites_groups.py +++ b/backend/gn_module_monitoring/routes/sites_groups.py @@ -3,18 +3,26 @@ from gn_module_monitoring.blueprint import blueprint from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups +from gn_module_monitoring.utils.routes import ( + filter_params, + get_limit_offset, + get_sort, + paginate, + sort, +) from gn_module_monitoring.monitoring.schemas import MonitoringSitesGroupsSchema -from gn_module_monitoring.utils.routes import filter_params, get_limit_offset, paginate @blueprint.route("/sites_groups", methods=["GET"]) def get_sites_groups(): params = MultiDict(request.args) limit, page = get_limit_offset(params=params) - + sort_label, sort_dir = get_sort( + params=params, default_sort="id_sites_group", default_direction="desc" + ) query = filter_params(query=TMonitoringSitesGroups.query, params=params) - query = query.order_by(TMonitoringSitesGroups.id_sites_group) + query = sort(query=query, sort=sort_label, sort_dir=sort_dir) return paginate( query=query, schema=MonitoringSitesGroupsSchema, diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_sites_groups.py b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_sites_groups.py new file mode 100644 index 000000000..99a54f811 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_sites_groups.py @@ -0,0 +1,36 @@ +import pytest + +from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups + + +@pytest.mark.usefixtures("temporary_transaction") +class TestTMonitoringSitesGroups: + def test_sort_desc(self, sites_groups): + if len(sites_groups) < 2: + pytest.xfail( + "This test cannot work if there is less than 2 sites_groups in database (via fixtures or not)" + ) + + query = TMonitoringSitesGroups.query.filter( + TMonitoringSitesGroups.id_sites_group.in_( + group.id_sites_group for group in sites_groups.values() + ) + ).sort(label="id_sites_group", direction="desc") + result = query.all() + + assert result[0].id_sites_group > result[1].id_sites_group + + def test_sort_asc(self, sites_groups): + if len(sites_groups) < 2: + pytest.xfail( + "This test cannot work if there is less than 2 sites_groups in database (via fixtures or not)" + ) + + query = TMonitoringSitesGroups.query.filter( + TMonitoringSitesGroups.id_sites_group.in_( + group.id_sites_group for group in sites_groups.values() + ) + ).sort(label="id_sites_group", direction="asc") + result = query.all() + + assert result[0].id_sites_group < result[1].id_sites_group diff --git a/backend/gn_module_monitoring/tests/test_routes/test_site.py b/backend/gn_module_monitoring/tests/test_routes/test_site.py index d63591ca5..ecbbb3b68 100644 --- a/backend/gn_module_monitoring/tests/test_routes/test_site.py +++ b/backend/gn_module_monitoring/tests/test_routes/test_site.py @@ -22,9 +22,7 @@ def test_get_categories(self, categories): r = self.client.get(url_for("monitorings.get_categories")) assert r.json["count"] >= len(categories) - assert all( - [schema.dump(cat) in r.json["items"] for cat in categories.values()] - ) + assert all([schema.dump(cat) in r.json["items"] for cat in categories.values()]) def test_get_categories_label(self, categories): label = list(categories.keys())[0] @@ -47,6 +45,24 @@ def test_get_sites_limit(self, sites): assert len(r.json["items"]) == limit + def test_get_sites_base_site_name(self, sites): + site = list(sites.values())[0] + base_site_name = site.base_site_name + + r = self.client.get(url_for("monitorings.get_sites", base_site_name=base_site_name)) + + assert len(r.json["items"]) == 1 + assert r.json["items"][0]["base_site_name"] == base_site_name + + def test_get_sites_id_base_site(self, sites): + site = list(sites.values())[0] + id_base_site = site.id_base_site + + r = self.client.get(url_for("monitorings.get_sites", id_base_site=id_base_site)) + + assert len(r.json["items"]) == 1 + assert r.json["items"][0]["id_base_site"] == id_base_site + def test_get_module_sites(self): module_code = "TEST" r = self.client.get(url_for("monitorings.get_module_sites", module_code=module_code)) diff --git a/backend/gn_module_monitoring/utils/routes.py b/backend/gn_module_monitoring/utils/routes.py index a7889dc68..0102f323c 100644 --- a/backend/gn_module_monitoring/utils/routes.py +++ b/backend/gn_module_monitoring/utils/routes.py @@ -6,6 +6,7 @@ from sqlalchemy.orm import Query from werkzeug.datastructures import MultiDict +from gn_module_monitoring.monitoring.queries import Query as MonitoringQuery from gn_module_monitoring.monitoring.schemas import paginate_schema @@ -13,6 +14,10 @@ def get_limit_offset(params: MultiDict) -> Tuple[int]: return int(params.pop("limit", 50)), int(params.pop("offset", 1)) +def get_sort(params: MultiDict, default_sort: str, default_direction) -> Tuple[str]: + return params.pop("sort", default_sort), params.pop("sort_dir", default_direction) + + def paginate(query: Query, schema: Schema, limit: int, page: int) -> Response: result = query.paginate(page=page, error_out=False, per_page=limit) pagination_schema = paginate_schema(schema) @@ -22,7 +27,13 @@ def paginate(query: Query, schema: Schema, limit: int, page: int) -> Response: return jsonify(data) -def filter_params(query: Query, params: MultiDict) -> Query: +def filter_params(query: MonitoringQuery, params: MultiDict) -> MonitoringQuery: if len(params) != 0: - query = query.filter_by(**params) + query = query.filter_by_params(params) + return query + + +def sort(query: MonitoringQuery, sort: str, sort_dir: str) -> MonitoringQuery: + if sort_dir in ["desc", "asc"]: + query = query.sort(label=sort, direction=sort_dir) return query From 5c93c0c92f83d6f28e804cde0e552d38269d42e6 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Thu, 5 Jan 2023 18:23:14 +0100 Subject: [PATCH 014/161] fix(config): changed categories into items (#29) * Merged feat/package-module-alembic commit 70b01395d5031487a4e74e9cbe60a87ceabe2a70 Author: Maxime Vergez Date: Tue Dec 6 15:22:56 2022 +0100 style: apply black commit 2badedf9976435319cc577ec3e4b861e1a4ec3c1 Author: Maxime Vergez Date: Tue Dec 6 15:11:41 2022 +0100 chore(db): removed unused import commit 57d9b523d620cc55580002a53f2883cdf692ba5f Author: Maxime Vergez Date: Tue Dec 6 14:47:48 2022 +0100 feat(db): improved & finished downgrade func commit 211780f4467f27f0e188623b03c46c25823c34bc Author: Maxime Vergez Date: Tue Dec 6 09:32:12 2022 +0100 feat(db): wip: add upgrade and began downgrade Downgrade migration need a little bit more work due to the use of sql alchemy and not plain text sql commit 0b5a3b883fe8f08e0f8d34198ab3c44b39728159 Author: Maxime Vergez Date: Mon Dec 5 11:40:32 2022 +0100 feat: packaged module * Feat/edit categories module (#19) * tests: make tests and fixtures work for modules By changing the way a monitoring module is created in the fixture Add two tests to check the relationship * feat(api): add categories in edit module * style: applied black * fix(config): changed categories into items Following the change with marshmallow schemas --- backend/gn_module_monitoring/config/generic/module.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/gn_module_monitoring/config/generic/module.json b/backend/gn_module_monitoring/config/generic/module.json index b84bf5f6b..2f1494d27 100644 --- a/backend/gn_module_monitoring/config/generic/module.json +++ b/backend/gn_module_monitoring/config/generic/module.json @@ -131,7 +131,7 @@ "api" : "__MONITORINGS_PATH/sites/categories", "application": "GeoNature", "required": true, - "data_path": "categories", + "data_path": "items", "definition": "Permet de paramétrer la compatibilité de ce module avec les catégories de sites" }, From 3185274aabfb944828bfb16382c771a5a442097e Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Fri, 13 Jan 2023 09:32:29 +0100 Subject: [PATCH 015/161] Fix/db migrations (#31) * feat(db): upgrade down_revision following rebase Since rebase with develop: changed the down_revision number * fix(db): fix bind params enabling downgrade Beforehand the downgrade was not possible... * refactor(db): removed cor_site_type_category * refactor(db): changed category into type in cor * refactor(db): create cor_type_site * fix(db): renamed column * refactor(api): update models to fit migrations * fix(db):change bib_categorie_site to bib_type_site Adding : cor_site_module cor_site_type revision alembic to create function and trigger in order to add bib_type_site but only with nomenclature 'TYPE_SITE' upgrade and downgrade works [Refs ticket]: #3 Reviewed-by: andriac * fix(api): updated models from migrations * fix(api): wip: fix admin following migrations * fix(api): update routes and tests To match migration changes * feat: flask admin bib_type_site Change bib_categories to bib_type_site into flask admin Adding filtering in list label_fr of type_site to secure the unique constraint Reviewed-by: andriac [Refs ticket]: #3 * fix(api): updated schema to match models * fix(api): module edition * style(api): uniformize type_site * style(api): change relationship name for type_site * feat(api): validator admin * fix(api): make unique BibTypeSite in admin * test(api): fix test when existing nomenclatures In database Co-authored-by: Andria Capai --- backend/gn_module_monitoring/blueprint.py | 4 +- .../config/generic/module.json | 12 ++-- ...remove_id_module_from_sites_complements.py | 4 +- ...=> a54bafb13ce8_create_cor_module_type.py} | 29 ++++---- .../b53bafb13ce8_create_bib_categorie_site.py | 60 ---------------- .../b53bafb13ce8_create_bib_type_site.py | 71 +++++++++++++++++++ .../ce54ba49ce5c_create_cor_type_site.py | 51 +++++++++++++ ...bafb13ce8_create_cor_site_type_category.py | 47 ------------ ...1f54_remove_id_module_from_sites_groups.py | 12 ++-- .../gn_module_monitoring/monitoring/admin.py | 42 +++++++---- .../gn_module_monitoring/monitoring/models.py | 50 ++++++++++--- .../monitoring/schemas.py | 15 ++-- .../gn_module_monitoring/routes/data_utils.py | 4 +- backend/gn_module_monitoring/routes/site.py | 27 ++++--- .../gn_module_monitoring/tests/conftest.py | 1 + .../tests/fixtures/module.py | 5 +- .../tests/fixtures/site.py | 38 ++-------- .../tests/fixtures/type_site.py | 40 +++++++++++ .../test_models/test_bib_type_site.py | 23 ++++++ .../test_models/test_module.py | 12 ++-- .../test_schemas/test_bib_site_type_schema.py | 14 ++++ .../tests/test_routes/test_site.py | 28 +++----- 22 files changed, 349 insertions(+), 240 deletions(-) rename backend/gn_module_monitoring/migrations/{a54bafb13ce8_create_cor_module_category.py => a54bafb13ce8_create_cor_module_type.py} (53%) delete mode 100644 backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_categorie_site.py create mode 100644 backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py create mode 100644 backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py delete mode 100644 backend/gn_module_monitoring/migrations/e64bafb13ce8_create_cor_site_type_category.py create mode 100644 backend/gn_module_monitoring/tests/fixtures/type_site.py create mode 100644 backend/gn_module_monitoring/tests/test_monitoring/test_models/test_bib_type_site.py create mode 100644 backend/gn_module_monitoring/tests/test_monitoring/test_schemas/test_bib_site_type_schema.py diff --git a/backend/gn_module_monitoring/blueprint.py b/backend/gn_module_monitoring/blueprint.py index 267b8fbba..116e2a03f 100644 --- a/backend/gn_module_monitoring/blueprint.py +++ b/backend/gn_module_monitoring/blueprint.py @@ -7,7 +7,7 @@ from geonature.core.admin.admin import admin as flask_admin from geonature.utils.env import DB -from gn_module_monitoring.monitoring.admin import BibCategorieSiteView +from gn_module_monitoring.monitoring.admin import BibTypeSiteView from .command.cmd import commands blueprint = Blueprint( @@ -19,4 +19,4 @@ for cmd in commands: blueprint.cli.add_command(cmd) -flask_admin.add_view(BibCategorieSiteView(DB.session, name="Catégories de sites", category="Monitorings")) +flask_admin.add_view(BibTypeSiteView(DB.session, name="Types de site", category="Monitorings")) diff --git a/backend/gn_module_monitoring/config/generic/module.json b/backend/gn_module_monitoring/config/generic/module.json index 2f1494d27..af77abbb1 100644 --- a/backend/gn_module_monitoring/config/generic/module.json +++ b/backend/gn_module_monitoring/config/generic/module.json @@ -121,18 +121,18 @@ "definition": "Afficher le module dans le menu de GeoNature. (Recharger la page pour voir les modifications)." }, - "categories": { + "types_site": { "type_widget": "datalist", - "attribut_label": "Catégories de sites", - "type_util": "categorie", - "keyValue": "id_categorie", + "attribut_label": "Types de sites", + "type_util": "types_site", + "keyValue": "id_nomenclature", "keyLabel": "label", "multiple": true, - "api" : "__MONITORINGS_PATH/sites/categories", + "api" : "__MONITORINGS_PATH/sites/types", "application": "GeoNature", "required": true, "data_path": "items", - "definition": "Permet de paramétrer la compatibilité de ce module avec les catégories de sites" + "definition": "Permet de paramétrer la compatibilité de ce module avec les types de sites" }, "medias": { diff --git a/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py b/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py index ca6d04879..418234cdd 100644 --- a/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py +++ b/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py @@ -1,7 +1,7 @@ """remove_id_module_from_sites_complements Revision ID: 6673266fb79c -Revises: +Revises: a54bafb13ce8 Create Date: 2022-12-13 16:00:00.512562 """ @@ -12,7 +12,7 @@ # revision identifiers, used by Alembic. revision = "6673266fb79c" -down_revision = "e64bafb13ce8" +down_revision = "a54bafb13ce8" branch_labels = None depends_on = None diff --git a/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_category.py b/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py similarity index 53% rename from backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_category.py rename to backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py index 491198526..718194469 100644 --- a/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_category.py +++ b/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py @@ -1,7 +1,7 @@ -"""create_cor_module_category +"""create_cor_module_type Revision ID: a54bafb13ce8 -Revises: +Revises: ce54ba49ce5c Create Date: 2022-12-06 16:18:24.512562 """ @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. revision = "a54bafb13ce8" -down_revision = "f24adb481f54" +down_revision = "ce54ba49ce5c" branch_labels = None depends_on = None @@ -20,28 +20,33 @@ def upgrade(): op.create_table( - "cor_module_categorie", + "cor_module_type", sa.Column( - "id_categorie", + "id_type_site", sa.Integer(), sa.ForeignKey( - f"{monitorings_schema}.bib_categorie_site.id_categorie", - name="fk_cor_module_categorie_id_categorie", + f"{monitorings_schema}.bib_type_site.id_nomenclature", + name="fk_cor_module_type_id_nomenclature", ondelete="CASCADE", onupdate="CASCADE", ), nullable=False, ), - sa.Column("id_module", sa.Integer(),sa.ForeignKey( + sa.Column( + "id_module", + sa.Integer(), + sa.ForeignKey( f"{referent_schema}.t_modules.id_module", - name="fk_cor_module_categorie_id_module", + name="fk_cor_module_type_id_module", ondelete="CASCADE", onupdate="CASCADE", - ), nullable=False), - sa.PrimaryKeyConstraint("id_categorie", "id_module", name="pk_cor_module_categorie"), + ), + nullable=False, + ), + sa.PrimaryKeyConstraint("id_type_site", "id_module", name="pk_cor_module_type"), schema=monitorings_schema, ) def downgrade(): - op.drop_table("cor_module_categorie", schema=monitorings_schema) + op.drop_table("cor_module_type", schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_categorie_site.py b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_categorie_site.py deleted file mode 100644 index 610269dd5..000000000 --- a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_categorie_site.py +++ /dev/null @@ -1,60 +0,0 @@ -"""create_bib_categorie_site - -Revision ID: b53bafb13ce8 -Revises: -Create Date: 2022-12-06 16:18:24.512562 - -""" -from alembic import op -import sqlalchemy as sa - -# revision identifiers, used by Alembic. -revision = "b53bafb13ce8" -down_revision = "362cf9d504ec" -branch_labels = None -depends_on = None - -monitorings_schema = "gn_monitoring" - - -def upgrade(): - op.create_table( - "bib_categorie_site", - sa.Column("id_categorie", sa.Integer(), nullable=False), - sa.Column("label", sa.String(), nullable=False), - sa.Column("config", sa.JSON(), nullable=True), - sa.PrimaryKeyConstraint("id_categorie"), - schema=monitorings_schema, - ) - op.create_index( - op.f("ix_bib_categorie_site_id"), - "bib_categorie_site", - ["id_categorie"], - unique=False, - schema=monitorings_schema, - ) - op.add_column( - "t_base_sites", - sa.Column( - "id_categorie", - sa.Integer(), - sa.ForeignKey( - f"{monitorings_schema}.bib_categorie_site.id_categorie", - name="fk_t_base_sites_id_categorie", - ondelete="CASCADE", - ), - nullable=True, # TODO: see migration? nullable is conservative here - ), - schema=monitorings_schema, - ) - - -def downgrade(): - op.drop_constraint("fk_t_base_sites_id_categorie", "t_base_sites", schema=monitorings_schema) - op.drop_column("t_base_sites", "id_categorie", schema=monitorings_schema) - op.drop_index( - op.f("ix_bib_categorie_site_id"), - table_name="bib_categorie_site", - schema=monitorings_schema, - ) - op.drop_table("bib_categorie_site", schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py new file mode 100644 index 000000000..a064d26bb --- /dev/null +++ b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py @@ -0,0 +1,71 @@ +"""create_bib_type_site + +Revision ID: b53bafb13ce8 +Revises: e78003460441 +Create Date: 2022-12-06 16:18:24.512562 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "b53bafb13ce8" +down_revision = "e78003460441" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" +nomenclature_schema = "ref_nomenclatures" + +TYPE_SITE = "TYPE_SITE" + + +def upgrade(): + op.create_table( + "bib_type_site", + sa.Column( + "id_nomenclature", + sa.Integer(), + sa.ForeignKey( + f"{nomenclature_schema}.t_nomenclatures.id_nomenclature", + name="fk_t_nomenclatures_id_nomenclature", + ), + nullable=False, + unique=True, + ), + sa.PrimaryKeyConstraint("id_nomenclature"), + sa.Column("config", sa.JSON(), nullable=True), + schema=monitorings_schema, + ) + + statement = sa.text( + f""" + CREATE OR REPLACE FUNCTION {monitorings_schema}.ck_bib_type_site_id_nomenclature() + RETURNS trigger + LANGUAGE plpgsql + AS $function$ + BEGIN + perform {nomenclature_schema}.check_nomenclature_type_by_mnemonique(NEW.id_nomenclature, :mnemonique ); + RETURN NEW; + END; + $function$ + ; + DROP TRIGGER IF EXISTS ck_bib_type_site_id_nomenclature on gn_monitoring.bib_type_site; + CREATE TRIGGER ck_bib_type_site_id_nomenclature BEFORE + INSERT + OR + UPDATE ON {monitorings_schema}.bib_type_site FOR EACH ROW EXECUTE PROCEDURE {monitorings_schema}.ck_bib_type_site_id_nomenclature(); + """ + ).bindparams(mnemonique=TYPE_SITE) + op.execute(statement) + + +def downgrade(): + + op.drop_table("bib_type_site", schema=monitorings_schema) + statement = sa.text( + f""" + DROP FUNCTION IF EXISTS {monitorings_schema}.ck_bib_type_site_id_nomenclature; + """ + ) + op.execute(statement) diff --git a/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py b/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py new file mode 100644 index 000000000..8befc1f68 --- /dev/null +++ b/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py @@ -0,0 +1,51 @@ +"""create_cor_type_site + +Revision ID: ce54ba49ce5c +Revises: b53bafb13ce8 +Create Date: 2022-12-06 16:18:24.512562 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "ce54ba49ce5c" +down_revision = "b53bafb13ce8" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" + + +def upgrade(): + op.create_table( + "cor_type_site", + sa.Column( + "id_type_site", + sa.Integer(), + sa.ForeignKey( + f"{monitorings_schema}.bib_type_site.id_nomenclature", + name="fk_cor_type_site_id_nomenclature", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=False, + ), + sa.Column( + "id_base_site", + sa.Integer(), + sa.ForeignKey( + f"{monitorings_schema}.t_base_sites.id_base_site", + name="fk_cor_type_site_id_base_site", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=False, + ), + sa.PrimaryKeyConstraint("id_type_site", "id_base_site", name="pk_cor_type_site"), + schema=monitorings_schema, + ) + + +def downgrade(): + op.drop_table("cor_type_site", 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 deleted file mode 100644 index 2dfcf3412..000000000 --- a/backend/gn_module_monitoring/migrations/e64bafb13ce8_create_cor_site_type_category.py +++ /dev/null @@ -1,47 +0,0 @@ -"""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/migrations/f24adb481f54_remove_id_module_from_sites_groups.py b/backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py index 52c45d8e7..9280f92ed 100644 --- a/backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py +++ b/backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py @@ -1,7 +1,7 @@ """remove_id_module_from_sites_groups Revision ID: f24adb481f54 -Revises: +Revises: 6673266fb79c Create Date: 2022-12-13 16:00:00.512562 """ @@ -12,7 +12,7 @@ # revision identifiers, used by Alembic. revision = "f24adb481f54" -down_revision = "b53bafb13ce8" +down_revision = "6673266fb79c" branch_labels = None depends_on = None @@ -47,8 +47,8 @@ def downgrade(): update {monitorings_schema}.t_sites_groups set id_module = (select id_module from gn_commons.t_modules tm - where module_code = '\:module_code'); + where module_code = :module_code); """ - ) - op.execute(statement, module_code=MODULE_CODE) - op.alter_column("t_sites_groups", "id_module", nullable=False) + ).bindparams(module_code=MODULE_CODE) + op.execute(statement) + op.alter_column("t_sites_groups", "id_module", nullable=False, schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/monitoring/admin.py b/backend/gn_module_monitoring/monitoring/admin.py index e7184b8ce..5406513ff 100644 --- a/backend/gn_module_monitoring/monitoring/admin.py +++ b/backend/gn_module_monitoring/monitoring/admin.py @@ -2,16 +2,33 @@ from geonature.core.admin.admin import CruvedProtectedMixin from geonature.utils.env import DB from pypnnomenclature.models import TNomenclatures, BibNomenclaturesTypes +from wtforms.validators import ValidationError -from gn_module_monitoring.monitoring.models import BibCategorieSite +from gn_module_monitoring.monitoring.models import BibTypeSite SITE_TYPE = "TYPE_SITE" -class BibCategorieSiteView(CruvedProtectedMixin, ModelView): +class Unique: + """ validator that checks field uniqueness """ + def __init__(self, model, field, message=None): + self.model = model + self.field = field + if not message: + message = u'A type is already created with this nomenclature' + self.message = message + + def __call__(self, form, field): + if field.object_data == field.data: + return + if self.model.query.filter(getattr(self.model, self.field) == getattr(field.data, self.field)).first(): + raise ValidationError(self.message) + + +class BibTypeSiteView(CruvedProtectedMixin, ModelView): """ - Surcharge de l'administration des catégories de sites + Surcharge de l'administration des types de sites """ module_code = "MONITORINGS" @@ -19,9 +36,9 @@ class BibCategorieSiteView(CruvedProtectedMixin, ModelView): def __init__(self, session, **kwargs): # Référence au model utilisé - super(BibCategorieSiteView, self).__init__(BibCategorieSite, session, **kwargs) + super(BibTypeSiteView, self).__init__(BibTypeSite, session, **kwargs) - def get_only_type_site_asc(): + def get_only_nomenclature_asc(): return ( DB.session.query(TNomenclatures) .join(TNomenclatures.nomenclature_type) @@ -32,19 +49,20 @@ def get_only_type_site_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] + def list_label_nomenclature_formatter(view, _context, model, _name): + return model.nomenclature.label_fr # Nom de colonne user friendly - column_labels = dict(site_type="Type de site") + column_labels = dict(nomenclature="Types de site") # Description des colonnes - column_descriptions = dict(site_type="Type de site à choisir en lien avec la catégorie") + column_descriptions = dict(nomenclature="Nomenclature de type de site à choisir") column_hide_backrefs = False form_args = dict( - site_type=dict(query_factory=get_only_type_site_asc, get_label=get_label_fr_nomenclature) + nomenclature=dict(query_factory=get_only_nomenclature_asc, get_label=get_label_fr_nomenclature, + validators=[Unique(BibTypeSite, "id_nomenclature")]) ) - column_list = ("label", "config", "site_type") - column_formatters = dict(site_type=list_label_site_type_formatter) + column_list = ("nomenclature","config") + column_formatters = dict(nomenclature=list_label_nomenclature_formatter) diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index 99d082d7e..936e9c418 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -19,11 +19,10 @@ from geonature.core.gn_commons.models import TModules, cor_module_dataset from pypnusershub.db.models import User from geonature.core.gn_monitoring.models import corVisitObserver - from gn_module_monitoring.monitoring.queries import Query as MonitoringQuery -cor_module_categorie = DB.Table( - "cor_module_categorie", +cor_module_type = DB.Table( + "cor_module_type", DB.Column( "id_module", DB.Integer, @@ -31,27 +30,44 @@ primary_key=True, ), DB.Column( - "id_categorie", + "id_type_site", DB.Integer, - DB.ForeignKey("gn_monitoring.bib_categorie_site.id_categorie"), + DB.ForeignKey("gn_monitoring.bib_type_site.id_nomenclature"), primary_key=True, ), schema="gn_monitoring") -cor_site_type_categorie = DB.Table( - "cor_site_type_categorie", +cor_type_site = DB.Table( + "cor_type_site", DB.Column( - "id_nomenclature", + "id_base_site", DB.Integer, - DB.ForeignKey("ref_nomenclatures.t_nomenclatures.id_nomenclature"), + DB.ForeignKey("gn_monitoring.t_base_sites.id_base_site"), primary_key=True, ), DB.Column( - "id_categorie", + "id_type_site", DB.Integer, - DB.ForeignKey("gn_monitoring.bib_categorie_site.id_categorie"), + DB.ForeignKey("gn_monitoring.bib_type_site.id_nomenclature"), primary_key=True, ), schema="gn_monitoring") + +@serializable +class BibTypeSite(DB.Model): + __tablename__ = "bib_type_site" + __table_args__ = {"schema": "gn_monitoring"} + query_class = MonitoringQuery + + id_nomenclature = DB.Column(DB.ForeignKey("ref_nomenclatures.t_nomenclatures.id_nomenclature"), + nullable=False, + primary_key=True) + config = DB.Column(JSONB) + nomenclature = DB.relationship( + TNomenclatures, + uselist=False, + backref=DB.backref('bib_type_site', uselist=False) + ) + @serializable class TMonitoringObservationDetails(DB.Model): __tablename__ = "t_observation_details" @@ -214,6 +230,11 @@ class TMonitoringSites(TBaseSites): .where(TBaseSites.id_base_site == id_base_site) .correlate_except(TBaseSites) ) + types_site = DB.relationship( + "BibTypeSite", + secondary=cor_type_site, + lazy="joined" + ) @serializable @@ -320,6 +341,13 @@ class TMonitoringModules(TModules): lazy="joined", ) + types_site = DB.relationship( + "BibTypeSite", + secondary=cor_module_type, + lazy="joined" + ) + + data = DB.Column(JSONB) # visits = DB.relationship( diff --git a/backend/gn_module_monitoring/monitoring/schemas.py b/backend/gn_module_monitoring/monitoring/schemas.py index 2721de0e8..085af2751 100644 --- a/backend/gn_module_monitoring/monitoring/schemas.py +++ b/backend/gn_module_monitoring/monitoring/schemas.py @@ -6,7 +6,7 @@ from pypnnomenclature.schemas import NomenclatureSchema from gn_module_monitoring.monitoring.models import ( - BibCategorieSite, + BibTypeSite, TMonitoringSites, TMonitoringSitesGroups, ) @@ -46,12 +46,15 @@ def serialize_geojson(self, obj): return geojson.dumps(obj.as_geofeature().get("geometry")) -class BibCategorieSiteSchema(SQLAlchemyAutoSchema): - site_type = fields.Nested( - NomenclatureSchema(only=("id_nomenclature", "label_fr")), many=True, dump_only=True - ) +class BibTypeSiteSchema(SQLAlchemyAutoSchema): + label = fields.Method("get_label_from_type_site") + # See if useful in the future: + # type_site = fields.Nested(NomenclatureSchema(only=("label_fr",)), dump_only=True) + + def get_label_from_type_site(self, obj): + return obj.nomenclature.label_fr class Meta: - model = BibCategorieSite + model = BibTypeSite include_fk = True load_instance = True diff --git a/backend/gn_module_monitoring/routes/data_utils.py b/backend/gn_module_monitoring/routes/data_utils.py index 2038f5f34..37e850231 100644 --- a/backend/gn_module_monitoring/routes/data_utils.py +++ b/backend/gn_module_monitoring/routes/data_utils.py @@ -32,7 +32,7 @@ from ..blueprint import blueprint from ..config.repositories import get_config -from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups, TMonitoringSites, BibCategorieSite +from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups, TMonitoringSites, BibTypeSite model_dict = { "habitat": Habref, @@ -40,7 +40,7 @@ "user": User, "taxonomy": Taxref, "dataset": TDatasets, - "categorie": BibCategorieSite, + "types_site": BibTypeSite, "observer_list": UserList, "taxonomy_list": BibListes, "sites_group": TMonitoringSitesGroups, diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py index 1a53ca316..989858feb 100644 --- a/backend/gn_module_monitoring/routes/site.py +++ b/backend/gn_module_monitoring/routes/site.py @@ -1,10 +1,9 @@ from flask import request from flask.json import jsonify -from geonature.core.gn_monitoring.models import TBaseSites from werkzeug.datastructures import MultiDict from gn_module_monitoring.blueprint import blueprint -from gn_module_monitoring.monitoring.models import BibCategorieSite, TMonitoringSites +from gn_module_monitoring.monitoring.models import BibTypeSite, TMonitoringSites from gn_module_monitoring.utils.routes import ( filter_params, get_limit_offset, @@ -12,33 +11,33 @@ paginate, sort, ) -from gn_module_monitoring.monitoring.schemas import MonitoringSitesSchema,BibCategorieSiteSchema +from gn_module_monitoring.monitoring.schemas import MonitoringSitesSchema,BibTypeSiteSchema -@blueprint.route("/sites/categories", methods=["GET"]) -def get_categories(): +@blueprint.route("/sites/types", methods=["GET"]) +def get_types_site(): params = MultiDict(request.args) limit, page = get_limit_offset(params=params) sort_label, sort_dir = get_sort( - params=params, default_sort="id_categorie", default_direction="desc" + params=params, default_sort="id_nomenclature", default_direction="desc" ) - query = filter_params(query=BibCategorieSite.query, params=params) + query = filter_params(query=BibTypeSite.query, params=params) query = sort(query=query, sort=sort_label, sort_dir=sort_dir) return paginate( query=query, - schema=BibCategorieSiteSchema, + schema=BibTypeSiteSchema, limit=limit, page=page, ) -@blueprint.route("/sites/categories/", methods=["GET"]) -def get_categories_by_id(id_categorie): - query = BibCategorieSite.query.filter_by(id_categorie=id_categorie) +@blueprint.route("/sites/types/", methods=["GET"]) +def get_type_site_by_id(id_type_site): + query = BibTypeSite.query.filter_by(id_nomenclature=id_type_site) res = query.first() - schema = BibCategorieSiteSchema() + schema = BibTypeSiteSchema() return schema.dump(res) @@ -50,9 +49,7 @@ def get_sites(): sort_label, sort_dir = get_sort( params=params, default_sort="id_base_site", default_direction="desc" ) - query = TMonitoringSites.query.join( - BibCategorieSite, TMonitoringSites.id_categorie == BibCategorieSite.id_categorie - ) + query = TMonitoringSites.query query = filter_params(query=query, params=params) query = sort(query=query, sort=sort_label, sort_dir=sort_dir) return paginate( diff --git a/backend/gn_module_monitoring/tests/conftest.py b/backend/gn_module_monitoring/tests/conftest.py index 6b1107a1d..422312d22 100644 --- a/backend/gn_module_monitoring/tests/conftest.py +++ b/backend/gn_module_monitoring/tests/conftest.py @@ -5,4 +5,5 @@ "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", ] diff --git a/backend/gn_module_monitoring/tests/fixtures/module.py b/backend/gn_module_monitoring/tests/fixtures/module.py index 41d946e37..29f678f25 100644 --- a/backend/gn_module_monitoring/tests/fixtures/module.py +++ b/backend/gn_module_monitoring/tests/fixtures/module.py @@ -4,18 +4,17 @@ 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(categories): +def monitoring_module(types_site): t_monitoring_module = TMonitoringModules( module_code=uuid4(), module_label="test", active_frontend=True, active_backend=False, module_path="test", - categories=list(categories.values()), + types_site=list(types_site.values()), ) with db.session.begin_nested(): diff --git a/backend/gn_module_monitoring/tests/fixtures/site.py b/backend/gn_module_monitoring/tests/fixtures/site.py index 415ffe321..397b36703 100644 --- a/backend/gn_module_monitoring/tests/fixtures/site.py +++ b/backend/gn_module_monitoring/tests/fixtures/site.py @@ -1,45 +1,17 @@ import pytest from geoalchemy2.shape import from_shape from geonature.utils.env import db -from pypnnomenclature.models import BibNomenclaturesTypes, TNomenclatures from shapely.geometry import Point -from gn_module_monitoring.monitoring.models import BibCategorieSite, TMonitoringSites -from gn_module_monitoring.tests.fixtures.sites_groups import sites_groups +from gn_module_monitoring.monitoring.models import TMonitoringSites @pytest.fixture() -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} - - with db.session.begin_nested(): - db.session.add_all(categories.values()) - - return categories - - -@pytest.fixture() -def sites(users, categories, sites_groups): +def sites(users, types_site, sites_groups): user = users["user"] geom_4326 = from_shape(Point(43, 24), srid=4326) sites = {} - # TODO: get_nomenclature from label - site_type = TNomenclatures.query.filter( - BibNomenclaturesTypes.mnemonique == "TYPE_SITE", TNomenclatures.mnemonique == "Grotte" - ).one() - for i, key in enumerate(categories.keys()): + for i, key in enumerate(types_site.keys()): sites[key] = TMonitoringSites( id_inventor=user.id_role, id_digitiser=user.id_role, @@ -47,8 +19,8 @@ def sites(users, categories, sites_groups): base_site_description=f"Description{i}", base_site_code=f"Code{i}", geom=geom_4326, - id_nomenclature_type_site=site_type.id_nomenclature, - id_categorie=categories[key].id_categorie, + id_nomenclature_type_site=types_site[key].id_nomenclature, + types_site=[types_site[key]], id_sites_group=sites_groups["Site_Groupe"].id_sites_group, ) with db.session.begin_nested(): diff --git a/backend/gn_module_monitoring/tests/fixtures/type_site.py b/backend/gn_module_monitoring/tests/fixtures/type_site.py new file mode 100644 index 000000000..36d419c86 --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/type_site.py @@ -0,0 +1,40 @@ +import pytest +from geonature.utils.env import db +from pypnnomenclature.models import BibNomenclaturesTypes, TNomenclatures + +from gn_module_monitoring.monitoring.models import BibTypeSite + + +@pytest.fixture +def nomenclature_types_site(): + mnemoniques = ("Test_Grotte", "Test_Mine") + nomenclatures = [] + type_site = BibNomenclaturesTypes.query.filter( + BibNomenclaturesTypes.mnemonique == "TYPE_SITE" + ).first() + for mnemo in mnemoniques: + nomenclatures.append( + TNomenclatures( + id_type=type_site.id_type, + cd_nomenclature=mnemo, + label_default=mnemo, + label_fr=mnemo, + active=True, + ) + ) + with db.session.begin_nested(): + db.session.add_all(nomenclatures) + return nomenclatures + + +@pytest.fixture +def types_site(nomenclature_types_site): + types_site = { + nomenc_type_site.mnemonique: BibTypeSite( + id_nomenclature=nomenc_type_site.id_nomenclature, config={} + ) + for nomenc_type_site in nomenclature_types_site + } + with db.session.begin_nested(): + db.session.add_all(types_site.values()) + return types_site diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_bib_type_site.py b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_bib_type_site.py new file mode 100644 index 000000000..ede321732 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_bib_type_site.py @@ -0,0 +1,23 @@ +import pytest + +from gn_module_monitoring.monitoring.models import BibTypeSite + + +@pytest.mark.usefixtures("temporary_transaction") +class TestBibTypeSite: + def test_get_bib_type_site(self, types_site): + type_site = list(types_site.values())[0] + get_type_site = BibTypeSite.query.filter_by( + id_nomenclature=type_site.id_nomenclature + ).one() + + assert get_type_site.id_nomenclature == type_site.id_nomenclature + + def test_get_all_bib_type_site(self, types_site): + get_types_site = BibTypeSite.query.all() + + assert all( + type_site.id_nomenclature + in [get_type_site.id_nomenclature for get_type_site in get_types_site] + for type_site in types_site.values() + ) 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 index fbf606816..978b9d1ca 100644 --- 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 @@ -6,14 +6,14 @@ @pytest.mark.usefixtures("temporary_transaction") class TestModule: - def test_module(self, monitoring_module, categories): - cats = monitoring_module.categories - assert cats == list(categories.values()) + def test_module(self, monitoring_module, types_site): + types = monitoring_module.types_site + assert types == list(types_site.values()) - def test_remove_categorie_from_module(self, monitoring_module, categories): + def test_remove_categorie_from_module(self, monitoring_module, types_site): with db.session.begin_nested(): - monitoring_module.categories.pop(0) + monitoring_module.types_site.pop(0) mon = TMonitoringModules.query.filter_by(id_module=monitoring_module.id_module).one() - assert len(mon.categories) == len(categories) - 1 + assert len(mon.types_site) == len(types_site) - 1 diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_schemas/test_bib_site_type_schema.py b/backend/gn_module_monitoring/tests/test_monitoring/test_schemas/test_bib_site_type_schema.py new file mode 100644 index 000000000..e3852cdb1 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_schemas/test_bib_site_type_schema.py @@ -0,0 +1,14 @@ +import pytest + +from gn_module_monitoring.monitoring.models import BibTypeSite +from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema + + +@pytest.mark.usefixtures("temporary_transaction") +class TestBibSiteTypeSchema: + def test_dump(self, types_site): + one_type_site = BibTypeSite.query.first() + schema = BibTypeSiteSchema() + type_site = schema.dump(one_type_site) + + assert type_site["id_nomenclature"] == one_type_site.id_nomenclature diff --git a/backend/gn_module_monitoring/tests/test_routes/test_site.py b/backend/gn_module_monitoring/tests/test_routes/test_site.py index ecbbb3b68..59733d2c9 100644 --- a/backend/gn_module_monitoring/tests/test_routes/test_site.py +++ b/backend/gn_module_monitoring/tests/test_routes/test_site.py @@ -1,34 +1,28 @@ import pytest from flask import url_for -from gn_module_monitoring.monitoring.schemas import BibCategorieSiteSchema, MonitoringSitesSchema +from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema @pytest.mark.usefixtures("client_class", "temporary_transaction") class TestSite: - def test_get_categories_by_id(self, categories): - for cat in categories.values(): + def test_get_type_site_by_id(self, types_site): + for type_site in types_site.values(): r = self.client.get( url_for( - "monitorings.get_categories_by_id", - id_categorie=cat.id_categorie, + "monitorings.get_type_site_by_id", + id_type_site=type_site.id_nomenclature, ) ) - assert r.json["label"] == cat.label + assert r.json["id_nomenclature"] == type_site.id_nomenclature - def test_get_categories(self, categories): - schema = BibCategorieSiteSchema() + def test_get_types_site(self, types_site): + schema = BibTypeSiteSchema() - r = self.client.get(url_for("monitorings.get_categories")) + r = self.client.get(url_for("monitorings.get_types_site")) - assert r.json["count"] >= len(categories) - assert all([schema.dump(cat) in r.json["items"] for cat in categories.values()]) - - def test_get_categories_label(self, categories): - label = list(categories.keys())[0] - schema = BibCategorieSiteSchema() - r = self.client.get(url_for("monitorings.get_categories"), query_string={"label": label}) - assert schema.dump(categories[label]) in r.json["items"] + assert r.json["count"] >= len(types_site) + assert all([schema.dump(cat) in r.json["items"] for cat in types_site.values()]) def test_get_sites(self, sites): schema = MonitoringSitesSchema() From 9f8ce843ea3e335f51a3e1965b6a1e902b970683 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Fri, 13 Jan 2023 10:08:26 +0100 Subject: [PATCH 016/161] perf(api): improved loading of modules (#30) * Merged feat/package-module-alembic commit 70b01395d5031487a4e74e9cbe60a87ceabe2a70 Author: Maxime Vergez Date: Tue Dec 6 15:22:56 2022 +0100 style: apply black commit 2badedf9976435319cc577ec3e4b861e1a4ec3c1 Author: Maxime Vergez Date: Tue Dec 6 15:11:41 2022 +0100 chore(db): removed unused import commit 57d9b523d620cc55580002a53f2883cdf692ba5f Author: Maxime Vergez Date: Tue Dec 6 14:47:48 2022 +0100 feat(db): improved & finished downgrade func commit 211780f4467f27f0e188623b03c46c25823c34bc Author: Maxime Vergez Date: Tue Dec 6 09:32:12 2022 +0100 feat(db): wip: add upgrade and began downgrade Downgrade migration need a little bit more work due to the use of sql alchemy and not plain text sql commit 0b5a3b883fe8f08e0f8d34198ab3c44b39728159 Author: Maxime Vergez Date: Mon Dec 5 11:40:32 2022 +0100 feat: packaged module * perf(api): improved loading of modules When there are loads of datasets, the relationship takes a lot of time to load, and is useless for the route that uses it. * fix: due to wrong rebase Fix: newline on data_utils.py Fix: relationships are not joined anymore since there is no need of them Chore: update comment --- backend/gn_module_monitoring/modules/repositories.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/gn_module_monitoring/modules/repositories.py b/backend/gn_module_monitoring/modules/repositories.py index 75aaa6070..5f5f98f06 100644 --- a/backend/gn_module_monitoring/modules/repositories.py +++ b/backend/gn_module_monitoring/modules/repositories.py @@ -4,6 +4,7 @@ get_modules """ +from sqlalchemy.orm import Load from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound from geonature.utils.env import DB @@ -79,7 +80,15 @@ def get_modules(): """ try: - res = DB.session.query(TMonitoringModules).order_by(TMonitoringModules.module_label).all() + res = ( + DB.session.query(TMonitoringModules) + .options( + # Raise load not to load any relationship + Load(TMonitoringModules).raiseload("*") + ) + .order_by(TMonitoringModules.module_label) + .all() + ) return res From 71d90a17894ca57a1ce17136b607a3753f454084 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Fri, 13 Jan 2023 10:08:37 +0100 Subject: [PATCH 017/161] Fix/pagination (#28) * fix(api): page - 1 that returned the wrong page * test(api): add test for paginate --- .../tests/test_utils/test_routes.py | 15 ++++++++++++++- backend/gn_module_monitoring/utils/routes.py | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/backend/gn_module_monitoring/tests/test_utils/test_routes.py b/backend/gn_module_monitoring/tests/test_utils/test_routes.py index cc6357f37..39434141d 100644 --- a/backend/gn_module_monitoring/tests/test_utils/test_routes.py +++ b/backend/gn_module_monitoring/tests/test_utils/test_routes.py @@ -1,7 +1,9 @@ import pytest from werkzeug.datastructures import MultiDict -from gn_module_monitoring.utils.routes import get_limit_offset +from gn_module_monitoring.monitoring.models import TMonitoringSites +from gn_module_monitoring.monitoring.schemas import MonitoringSitesSchema +from gn_module_monitoring.utils.routes import get_limit_offset, paginate @pytest.mark.parametrize("limit, offset", [("1", "2"), (1, 2), ("1", 2), (1, "2")]) @@ -12,3 +14,14 @@ def test_get_limit_offset(limit, offset): assert isinstance(comp_limit, int) assert isinstance(comp_offset, int) + + +def test_paginate(sites): + limit = 1 + page = 2 + + res = paginate( + query=TMonitoringSites.query, schema=MonitoringSitesSchema, limit=limit, page=page + ) + + assert res.json["offset"] == page diff --git a/backend/gn_module_monitoring/utils/routes.py b/backend/gn_module_monitoring/utils/routes.py index 0102f323c..e5adabab0 100644 --- a/backend/gn_module_monitoring/utils/routes.py +++ b/backend/gn_module_monitoring/utils/routes.py @@ -22,7 +22,7 @@ def paginate(query: Query, schema: Schema, limit: int, page: int) -> Response: result = query.paginate(page=page, error_out=False, per_page=limit) pagination_schema = paginate_schema(schema) data = pagination_schema().dump( - dict(items=result.items, count=result.total, limit=limit, offset=page - 1) + dict(items=result.items, count=result.total, limit=limit, offset=page) ) return jsonify(data) From 07110bad561d1c85a2f470d9c8ce574665862d96 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Fri, 13 Jan 2023 10:36:56 +0100 Subject: [PATCH 018/161] style(api): restore data_utils spaces (#33) --- backend/gn_module_monitoring/routes/data_utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/gn_module_monitoring/routes/data_utils.py b/backend/gn_module_monitoring/routes/data_utils.py index 37e850231..805ea6a4f 100644 --- a/backend/gn_module_monitoring/routes/data_utils.py +++ b/backend/gn_module_monitoring/routes/data_utils.py @@ -3,6 +3,7 @@ d'utilisateurs de nomenclature de taxonomie + TODO cache """ @@ -115,6 +116,7 @@ def get_util_nomenclature_api(code_nomenclature_type, cd_nomenclature): revoie un champ d'un object de type nomenclature à partir de son type et de son cd_nomenclature renvoie l'objet entier si field_name renseigné en paramètre de route est 'all' + :param code_nomenclature_type: :param cd_nomenclature: :return object entier si field_name = all, la valeur du champs defini par field_name sinon @@ -164,6 +166,7 @@ def get_util_from_id_api(type_util, id): """ revoie un champ d'un object de type nomenclature, taxonomy, utilisateur, ... renvoie l'objet entier si field_name renseigné en paramètre de route est 'all' + :param type_util: 'nomenclature' | 'taxonomy' | 'utilisateur' | etc.... :param id: id de l'object requis :type type_util: str @@ -207,6 +210,7 @@ def get_util_from_ids_api(type_util, ids): """ variante de get_util_from_id_api pour plusieurs id renvoie un tableau de valeur (ou de dictionnaire si key est 'all') + parametre get key: all renvoie tout l'objet sinon renvoie un champ @@ -214,11 +218,13 @@ def get_util_from_ids_api(type_util, ids): pour reformer une chaine de caractere a partir du tableau résultat de la requete si separator_out == ' ,' alors ['jean', 'pierre', 'paul'].join(separator_out) -> 'jean, pierre, paul' + :param type_util: 'nomenclature' | 'taxonomy' | 'utilisateur' :param ids: plusieurs id reliée par des '-' (ex: 1-123-3-4) :type type_util: str :type ids: str :return list si key=all ou chaine de caratere + """ field_name = request.args.get("field_name", "all") @@ -244,4 +250,4 @@ def get_util_from_ids_api(type_util, ids): return [r.as_dict() for r in res] # renvoie une chaine de caratère - return separator_out.join([r[0] for r in res]) \ No newline at end of file + return separator_out.join([r[0] for r in res]) From c237beef146ac463de7280f8194f16f15d893e39 Mon Sep 17 00:00:00 2001 From: andriacap <111564663+andriacap@users.noreply.github.com> Date: Mon, 16 Jan 2023 16:29:48 +0100 Subject: [PATCH 019/161] Fix/db migrations checkconstrainton bib_type_site.id_nomenclature (#34) * fix(db): change trigger to constraint (migrations) Delete the trigger and create check constraint on id_nomenclature column Reviewed-by: andriacap [Refs ticket]: #3 * fix(db) : apply black on migration file Apply black [Refs_ticket]: #3 --- .../b53bafb13ce8_create_bib_type_site.py | 35 ++++--------------- 1 file changed, 6 insertions(+), 29 deletions(-) diff --git a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py index a064d26bb..91c8de459 100644 --- a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py +++ b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py @@ -17,8 +17,6 @@ monitorings_schema = "gn_monitoring" nomenclature_schema = "ref_nomenclatures" -TYPE_SITE = "TYPE_SITE" - def upgrade(): op.create_table( @@ -38,34 +36,13 @@ def upgrade(): schema=monitorings_schema, ) - statement = sa.text( - f""" - CREATE OR REPLACE FUNCTION {monitorings_schema}.ck_bib_type_site_id_nomenclature() - RETURNS trigger - LANGUAGE plpgsql - AS $function$ - BEGIN - perform {nomenclature_schema}.check_nomenclature_type_by_mnemonique(NEW.id_nomenclature, :mnemonique ); - RETURN NEW; - END; - $function$ - ; - DROP TRIGGER IF EXISTS ck_bib_type_site_id_nomenclature on gn_monitoring.bib_type_site; - CREATE TRIGGER ck_bib_type_site_id_nomenclature BEFORE - INSERT - OR - UPDATE ON {monitorings_schema}.bib_type_site FOR EACH ROW EXECUTE PROCEDURE {monitorings_schema}.ck_bib_type_site_id_nomenclature(); - """ - ).bindparams(mnemonique=TYPE_SITE) - op.execute(statement) + op.create_check_constraint( + "ck_bib_type_site_id_nomenclature", + "bib_type_site", + f"{nomenclature_schema}.check_nomenclature_type_by_mnemonique(id_nomenclature,'TYPE_SITE')", + schema=monitorings_schema, + ) def downgrade(): - op.drop_table("bib_type_site", schema=monitorings_schema) - statement = sa.text( - f""" - DROP FUNCTION IF EXISTS {monitorings_schema}.ck_bib_type_site_id_nomenclature; - """ - ) - op.execute(statement) From 57f15ab81d7ef57b0b7f849fe61f0b9886a866da Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Tue, 17 Jan 2023 09:39:59 +0100 Subject: [PATCH 020/161] fix(api): invert filter condition with Unicode (#35) So that it will fallback to == most of the time and ilike when just Unicode --- backend/gn_module_monitoring/monitoring/queries.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/gn_module_monitoring/monitoring/queries.py b/backend/gn_module_monitoring/monitoring/queries.py index cc2d6f972..768fe2ad1 100644 --- a/backend/gn_module_monitoring/monitoring/queries.py +++ b/backend/gn_module_monitoring/monitoring/queries.py @@ -1,5 +1,5 @@ from flask_sqlalchemy import BaseQuery -from sqlalchemy import Integer, and_ +from sqlalchemy import Unicode, and_ from werkzeug.datastructures import MultiDict @@ -21,10 +21,10 @@ def filter_by_params(self, params: MultiDict = None): and_list = [] for key, value in params.items(): column = getattr(model, key) - if isinstance(column.type, Integer): - and_list.append(column == value) - else: + if isinstance(column.type, Unicode): and_list.append(column.ilike(f"%{value}%")) + else: + and_list.append(column == value) and_query = and_(*and_list) return self.filter(and_query) From 32ddaf31af9adf7310a0ac56f4b846c3640fc224 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Tue, 17 Jan 2023 10:02:58 +0100 Subject: [PATCH 021/161] fix(db): add NOT VALID in constraint for bib_type_site (#36) Cannot use alembic to do this because need sqlalchemy >= 1.4.32 --- .../b53bafb13ce8_create_bib_type_site.py | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py index 91c8de459..f0a4f26f3 100644 --- a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py +++ b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py @@ -36,12 +36,26 @@ def upgrade(): schema=monitorings_schema, ) - op.create_check_constraint( - "ck_bib_type_site_id_nomenclature", - "bib_type_site", - f"{nomenclature_schema}.check_nomenclature_type_by_mnemonique(id_nomenclature,'TYPE_SITE')", - schema=monitorings_schema, + # FIXME: if sqlalchemy >= 1.4.32, it should work with postgresql_not_valid=True: cleaner + # op.create_check_constraint( + # "ck_bib_type_site_id_nomenclature", + # "bib_type_site", + # f"{nomenclature_schema}.check_nomenclature_type_by_mnemonique(id_nomenclature,'TYPE_SITE')", + # schema=monitorings_schema, + # postgresql_not_valid=True + # ) + statement = sa.text( + f""" + ALTER TABLE {monitorings_schema}.bib_type_site + ADD + CONSTRAINT ck_bib_type_site_id_nomenclature CHECK ( + {nomenclature_schema}.check_nomenclature_type_by_mnemonique( + id_nomenclature, 'TYPE_SITE' :: character varying + ) + ) NOT VALID + """ ) + op.execute(statement) def downgrade(): From 8435749c390ef71b29a762c0915736edbee2b2cd Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Wed, 18 Jan 2023 10:22:03 +0100 Subject: [PATCH 022/161] Fix/review checkpoint1 (#37) * refactor(api): change offset to page * refactor(api): rename id_nomenclature * fix(admin): add compare field to Unique Since now, BibTypeSite and TNomenclature do not share the same column anymore (id_nomenclature_type_site vs id_nomenclature) --- .../a54bafb13ce8_create_cor_module_type.py | 4 ++-- .../b53bafb13ce8_create_bib_type_site.py | 14 ++++++------- .../ce54ba49ce5c_create_cor_type_site.py | 4 ++-- .../gn_module_monitoring/monitoring/admin.py | 21 ++++++++++++------- .../gn_module_monitoring/monitoring/models.py | 6 +++--- .../monitoring/schemas.py | 2 +- backend/gn_module_monitoring/routes/site.py | 10 ++++----- .../routes/sites_groups.py | 4 ++-- .../tests/fixtures/site.py | 2 +- .../tests/fixtures/type_site.py | 2 +- .../test_models/test_bib_type_site.py | 8 +++---- .../test_schemas/test_bib_site_type_schema.py | 2 +- .../tests/test_routes/test_site.py | 4 ++-- .../tests/test_utils/test_routes.py | 14 ++++++------- backend/gn_module_monitoring/utils/routes.py | 6 +++--- 15 files changed, 55 insertions(+), 48 deletions(-) diff --git a/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py b/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py index 718194469..5bb62791f 100644 --- a/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py +++ b/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py @@ -25,8 +25,8 @@ def upgrade(): "id_type_site", sa.Integer(), sa.ForeignKey( - f"{monitorings_schema}.bib_type_site.id_nomenclature", - name="fk_cor_module_type_id_nomenclature", + f"{monitorings_schema}.bib_type_site.id_nomenclature_type_site", + name="fk_cor_module_type_id_nomenclature_type_site", ondelete="CASCADE", onupdate="CASCADE", ), diff --git a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py index f0a4f26f3..b079edf9e 100644 --- a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py +++ b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py @@ -22,25 +22,25 @@ def upgrade(): op.create_table( "bib_type_site", sa.Column( - "id_nomenclature", + "id_nomenclature_type_site", sa.Integer(), sa.ForeignKey( f"{nomenclature_schema}.t_nomenclatures.id_nomenclature", - name="fk_t_nomenclatures_id_nomenclature", + name="fk_t_nomenclatures_id_nomenclature_type_site", ), nullable=False, unique=True, ), - sa.PrimaryKeyConstraint("id_nomenclature"), + sa.PrimaryKeyConstraint("id_nomenclature_type_site"), sa.Column("config", sa.JSON(), nullable=True), schema=monitorings_schema, ) # FIXME: if sqlalchemy >= 1.4.32, it should work with postgresql_not_valid=True: cleaner # op.create_check_constraint( - # "ck_bib_type_site_id_nomenclature", + # "ck_bib_type_site_id_nomenclature_type_site", # "bib_type_site", - # f"{nomenclature_schema}.check_nomenclature_type_by_mnemonique(id_nomenclature,'TYPE_SITE')", + # f"{nomenclature_schema}.check_nomenclature_type_by_mnemonique(id_nomenclature_type_site,'TYPE_SITE')", # schema=monitorings_schema, # postgresql_not_valid=True # ) @@ -48,9 +48,9 @@ def upgrade(): f""" ALTER TABLE {monitorings_schema}.bib_type_site ADD - CONSTRAINT ck_bib_type_site_id_nomenclature CHECK ( + CONSTRAINT ck_bib_type_site_id_nomenclature_type_site CHECK ( {nomenclature_schema}.check_nomenclature_type_by_mnemonique( - id_nomenclature, 'TYPE_SITE' :: character varying + id_nomenclature_type_site, 'TYPE_SITE' :: character varying ) ) NOT VALID """ diff --git a/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py b/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py index 8befc1f68..f02e267e8 100644 --- a/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py +++ b/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py @@ -24,8 +24,8 @@ def upgrade(): "id_type_site", sa.Integer(), sa.ForeignKey( - f"{monitorings_schema}.bib_type_site.id_nomenclature", - name="fk_cor_type_site_id_nomenclature", + f"{monitorings_schema}.bib_type_site.id_nomenclature_type_site", + name="fk_cor_type_site_id_nomenclature_type_site", ondelete="CASCADE", onupdate="CASCADE", ), diff --git a/backend/gn_module_monitoring/monitoring/admin.py b/backend/gn_module_monitoring/monitoring/admin.py index 5406513ff..5e77d0a0a 100644 --- a/backend/gn_module_monitoring/monitoring/admin.py +++ b/backend/gn_module_monitoring/monitoring/admin.py @@ -11,18 +11,22 @@ class Unique: - """ validator that checks field uniqueness """ - def __init__(self, model, field, message=None): + """validator that checks field uniqueness""" + + def __init__(self, model, field, compare_field, message=None): self.model = model self.field = field + self.compare_field = compare_field if not message: - message = u'A type is already created with this nomenclature' + message = "A type is already created with this nomenclature" self.message = message def __call__(self, form, field): if field.object_data == field.data: return - if self.model.query.filter(getattr(self.model, self.field) == getattr(field.data, self.field)).first(): + if self.model.query.filter( + getattr(self.model, self.field) == getattr(field.data, self.compare_field) + ).first(): raise ValidationError(self.message) @@ -60,9 +64,12 @@ def list_label_nomenclature_formatter(view, _context, model, _name): column_hide_backrefs = False form_args = dict( - nomenclature=dict(query_factory=get_only_nomenclature_asc, get_label=get_label_fr_nomenclature, - validators=[Unique(BibTypeSite, "id_nomenclature")]) + nomenclature=dict( + query_factory=get_only_nomenclature_asc, + get_label=get_label_fr_nomenclature, + validators=[Unique(BibTypeSite, "id_nomenclature_type_site", "id_nomenclature")], + ) ) - column_list = ("nomenclature","config") + column_list = ("nomenclature", "config") column_formatters = dict(nomenclature=list_label_nomenclature_formatter) diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index 936e9c418..65160fbac 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -32,7 +32,7 @@ DB.Column( "id_type_site", DB.Integer, - DB.ForeignKey("gn_monitoring.bib_type_site.id_nomenclature"), + DB.ForeignKey("gn_monitoring.bib_type_site.id_nomenclature_type_site"), primary_key=True, ), schema="gn_monitoring") @@ -47,7 +47,7 @@ DB.Column( "id_type_site", DB.Integer, - DB.ForeignKey("gn_monitoring.bib_type_site.id_nomenclature"), + DB.ForeignKey("gn_monitoring.bib_type_site.id_nomenclature_type_site"), primary_key=True, ), schema="gn_monitoring") @@ -58,7 +58,7 @@ class BibTypeSite(DB.Model): __table_args__ = {"schema": "gn_monitoring"} query_class = MonitoringQuery - id_nomenclature = DB.Column(DB.ForeignKey("ref_nomenclatures.t_nomenclatures.id_nomenclature"), + id_nomenclature_type_site = DB.Column(DB.ForeignKey("ref_nomenclatures.t_nomenclatures.id_nomenclature"), nullable=False, primary_key=True) config = DB.Column(JSONB) diff --git a/backend/gn_module_monitoring/monitoring/schemas.py b/backend/gn_module_monitoring/monitoring/schemas.py index 085af2751..f34066c55 100644 --- a/backend/gn_module_monitoring/monitoring/schemas.py +++ b/backend/gn_module_monitoring/monitoring/schemas.py @@ -16,7 +16,7 @@ def paginate_schema(schema): class PaginationSchema(Schema): count = fields.Integer() limit = fields.Integer() - offset = fields.Integer() + page = fields.Integer() items = fields.Nested(schema, many=True, dump_only=True) return PaginationSchema diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py index 989858feb..92a209ad0 100644 --- a/backend/gn_module_monitoring/routes/site.py +++ b/backend/gn_module_monitoring/routes/site.py @@ -6,7 +6,7 @@ from gn_module_monitoring.monitoring.models import BibTypeSite, TMonitoringSites from gn_module_monitoring.utils.routes import ( filter_params, - get_limit_offset, + get_limit_page, get_sort, paginate, sort, @@ -17,9 +17,9 @@ @blueprint.route("/sites/types", methods=["GET"]) def get_types_site(): params = MultiDict(request.args) - limit, page = get_limit_offset(params=params) + limit, page = get_limit_page(params=params) sort_label, sort_dir = get_sort( - params=params, default_sort="id_nomenclature", default_direction="desc" + params=params, default_sort="id_nomenclature_type_site", default_direction="desc" ) query = filter_params(query=BibTypeSite.query, params=params) @@ -35,7 +35,7 @@ def get_types_site(): @blueprint.route("/sites/types/", methods=["GET"]) def get_type_site_by_id(id_type_site): - query = BibTypeSite.query.filter_by(id_nomenclature=id_type_site) + query = BibTypeSite.query.filter_by(id_nomenclature_type_site=id_type_site) res = query.first() schema = BibTypeSiteSchema() return schema.dump(res) @@ -45,7 +45,7 @@ def get_type_site_by_id(id_type_site): def get_sites(): params = MultiDict(request.args) # TODO: add filter support - limit, page = get_limit_offset(params=params) + limit, page = get_limit_page(params=params) sort_label, sort_dir = get_sort( params=params, default_sort="id_base_site", default_direction="desc" ) diff --git a/backend/gn_module_monitoring/routes/sites_groups.py b/backend/gn_module_monitoring/routes/sites_groups.py index 57317445d..e999c2351 100644 --- a/backend/gn_module_monitoring/routes/sites_groups.py +++ b/backend/gn_module_monitoring/routes/sites_groups.py @@ -5,7 +5,7 @@ from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups from gn_module_monitoring.utils.routes import ( filter_params, - get_limit_offset, + get_limit_page, get_sort, paginate, sort, @@ -16,7 +16,7 @@ @blueprint.route("/sites_groups", methods=["GET"]) def get_sites_groups(): params = MultiDict(request.args) - limit, page = get_limit_offset(params=params) + limit, page = get_limit_page(params=params) sort_label, sort_dir = get_sort( params=params, default_sort="id_sites_group", default_direction="desc" ) diff --git a/backend/gn_module_monitoring/tests/fixtures/site.py b/backend/gn_module_monitoring/tests/fixtures/site.py index 397b36703..e3d16d973 100644 --- a/backend/gn_module_monitoring/tests/fixtures/site.py +++ b/backend/gn_module_monitoring/tests/fixtures/site.py @@ -19,7 +19,7 @@ def sites(users, types_site, sites_groups): base_site_description=f"Description{i}", base_site_code=f"Code{i}", geom=geom_4326, - id_nomenclature_type_site=types_site[key].id_nomenclature, + id_nomenclature_type_site=types_site[key].id_nomenclature_type_site, types_site=[types_site[key]], id_sites_group=sites_groups["Site_Groupe"].id_sites_group, ) diff --git a/backend/gn_module_monitoring/tests/fixtures/type_site.py b/backend/gn_module_monitoring/tests/fixtures/type_site.py index 36d419c86..1a213a037 100644 --- a/backend/gn_module_monitoring/tests/fixtures/type_site.py +++ b/backend/gn_module_monitoring/tests/fixtures/type_site.py @@ -31,7 +31,7 @@ def nomenclature_types_site(): def types_site(nomenclature_types_site): types_site = { nomenc_type_site.mnemonique: BibTypeSite( - id_nomenclature=nomenc_type_site.id_nomenclature, config={} + id_nomenclature_type_site=nomenc_type_site.id_nomenclature, config={} ) for nomenc_type_site in nomenclature_types_site } diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_bib_type_site.py b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_bib_type_site.py index ede321732..ae580ccbf 100644 --- a/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_bib_type_site.py +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_bib_type_site.py @@ -8,16 +8,16 @@ class TestBibTypeSite: def test_get_bib_type_site(self, types_site): type_site = list(types_site.values())[0] get_type_site = BibTypeSite.query.filter_by( - id_nomenclature=type_site.id_nomenclature + id_nomenclature_type_site=type_site.id_nomenclature_type_site ).one() - assert get_type_site.id_nomenclature == type_site.id_nomenclature + assert get_type_site.id_nomenclature_type_site == type_site.id_nomenclature_type_site def test_get_all_bib_type_site(self, types_site): get_types_site = BibTypeSite.query.all() assert all( - type_site.id_nomenclature - in [get_type_site.id_nomenclature for get_type_site in get_types_site] + type_site.id_nomenclature_type_site + in [get_type_site.id_nomenclature_type_site for get_type_site in get_types_site] for type_site in types_site.values() ) diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_schemas/test_bib_site_type_schema.py b/backend/gn_module_monitoring/tests/test_monitoring/test_schemas/test_bib_site_type_schema.py index e3852cdb1..91aa19e0f 100644 --- a/backend/gn_module_monitoring/tests/test_monitoring/test_schemas/test_bib_site_type_schema.py +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_schemas/test_bib_site_type_schema.py @@ -11,4 +11,4 @@ def test_dump(self, types_site): schema = BibTypeSiteSchema() type_site = schema.dump(one_type_site) - assert type_site["id_nomenclature"] == one_type_site.id_nomenclature + assert type_site["id_nomenclature_type_site"] == one_type_site.id_nomenclature_type_site diff --git a/backend/gn_module_monitoring/tests/test_routes/test_site.py b/backend/gn_module_monitoring/tests/test_routes/test_site.py index 59733d2c9..07448b1a3 100644 --- a/backend/gn_module_monitoring/tests/test_routes/test_site.py +++ b/backend/gn_module_monitoring/tests/test_routes/test_site.py @@ -11,10 +11,10 @@ def test_get_type_site_by_id(self, types_site): r = self.client.get( url_for( "monitorings.get_type_site_by_id", - id_type_site=type_site.id_nomenclature, + id_type_site=type_site.id_nomenclature_type_site, ) ) - assert r.json["id_nomenclature"] == type_site.id_nomenclature + assert r.json["id_nomenclature_type_site"] == type_site.id_nomenclature_type_site def test_get_types_site(self, types_site): schema = BibTypeSiteSchema() diff --git a/backend/gn_module_monitoring/tests/test_utils/test_routes.py b/backend/gn_module_monitoring/tests/test_utils/test_routes.py index 39434141d..e81cb8017 100644 --- a/backend/gn_module_monitoring/tests/test_utils/test_routes.py +++ b/backend/gn_module_monitoring/tests/test_utils/test_routes.py @@ -3,17 +3,17 @@ from gn_module_monitoring.monitoring.models import TMonitoringSites from gn_module_monitoring.monitoring.schemas import MonitoringSitesSchema -from gn_module_monitoring.utils.routes import get_limit_offset, paginate +from gn_module_monitoring.utils.routes import get_limit_page, paginate -@pytest.mark.parametrize("limit, offset", [("1", "2"), (1, 2), ("1", 2), (1, "2")]) -def test_get_limit_offset(limit, offset): - multi_dict = MultiDict([("limit", limit), ("offset", offset)]) +@pytest.mark.parametrize("limit, page", [("1", "2"), (1, 2), ("1", 2), (1, "2")]) +def test_get_limit_page(limit, page): + multi_dict = MultiDict([("limit", limit), ("page", page)]) - comp_limit, comp_offset = get_limit_offset(params=multi_dict) + comp_limit, comp_page = get_limit_page(params=multi_dict) assert isinstance(comp_limit, int) - assert isinstance(comp_offset, int) + assert isinstance(comp_page, int) def test_paginate(sites): @@ -24,4 +24,4 @@ def test_paginate(sites): query=TMonitoringSites.query, schema=MonitoringSitesSchema, limit=limit, page=page ) - assert res.json["offset"] == page + assert res.json["page"] == page diff --git a/backend/gn_module_monitoring/utils/routes.py b/backend/gn_module_monitoring/utils/routes.py index e5adabab0..473f8500b 100644 --- a/backend/gn_module_monitoring/utils/routes.py +++ b/backend/gn_module_monitoring/utils/routes.py @@ -10,8 +10,8 @@ from gn_module_monitoring.monitoring.schemas import paginate_schema -def get_limit_offset(params: MultiDict) -> Tuple[int]: - return int(params.pop("limit", 50)), int(params.pop("offset", 1)) +def get_limit_page(params: MultiDict) -> Tuple[int]: + return int(params.pop("limit", 50)), int(params.pop("page", 1)) def get_sort(params: MultiDict, default_sort: str, default_direction) -> Tuple[str]: @@ -22,7 +22,7 @@ def paginate(query: Query, schema: Schema, limit: int, page: int) -> Response: result = query.paginate(page=page, error_out=False, per_page=limit) pagination_schema = paginate_schema(schema) data = pagination_schema().dump( - dict(items=result.items, count=result.total, limit=limit, offset=page) + dict(items=result.items, count=result.total, limit=limit, page=page) ) return jsonify(data) From c408e00ec6f7fc37bb25085e72099164d601a6d4 Mon Sep 17 00:00:00 2001 From: amandine-sahl Date: Fri, 3 Mar 2023 15:09:43 +0100 Subject: [PATCH 023/161] Add comments on new table --- .../migrations/a54bafb13ce8_create_cor_module_type.py | 5 +++++ .../migrations/b53bafb13ce8_create_bib_type_site.py | 8 +++++++- .../migrations/ce54ba49ce5c_create_cor_type_site.py | 5 +++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py b/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py index 5bb62791f..c1f55ac95 100644 --- a/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py +++ b/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py @@ -46,6 +46,11 @@ def upgrade(): sa.PrimaryKeyConstraint("id_type_site", "id_module", name="pk_cor_module_type"), schema=monitorings_schema, ) + op.create_table_comment( + "cor_module_type", + "Table d'association des types de sites potentiellement associés à un module", + schema=monitorings_schema + ) def downgrade(): diff --git a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py index b079edf9e..c45da8c58 100644 --- a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py +++ b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py @@ -46,7 +46,7 @@ def upgrade(): # ) statement = sa.text( f""" - ALTER TABLE {monitorings_schema}.bib_type_site + ALTER TABLE {monitorings_schema}.bib_type_site ADD CONSTRAINT ck_bib_type_site_id_nomenclature_type_site CHECK ( {nomenclature_schema}.check_nomenclature_type_by_mnemonique( @@ -56,6 +56,12 @@ def upgrade(): """ ) op.execute(statement) + op.create_table_comment( + "bib_type_site", + "Table de définition des champs associés aux types de sites", + schema=monitorings_schema, + ) + def downgrade(): diff --git a/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py b/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py index f02e267e8..ea470747b 100644 --- a/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py +++ b/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py @@ -45,6 +45,11 @@ def upgrade(): sa.PrimaryKeyConstraint("id_type_site", "id_base_site", name="pk_cor_type_site"), schema=monitorings_schema, ) + op.create_table_comment( + "cor_type_site", + "Table d'association entre les sites et les types de sites", + schema=monitorings_schema + ) def downgrade(): From 4b2ffad8fc74506d1dc68962d2e206ca5b10445a Mon Sep 17 00:00:00 2001 From: amandine-sahl Date: Fri, 3 Mar 2023 15:50:25 +0100 Subject: [PATCH 024/161] improve downgrade migration --- ...remove_id_module_from_sites_complements.py | 31 +++++++++++++++---- ...1f54_remove_id_module_from_sites_groups.py | 29 ++++++++++++++--- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py b/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py index 418234cdd..535789c4c 100644 --- a/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py +++ b/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py @@ -20,6 +20,20 @@ def upgrade(): + # Transfert data to core_site_module table + statement = sa.text( + f""" + INSERT INTO {monitorings_schema}.cor_site_module (id_module, id_base_site) + SELECT tsc.id_module, tsc.id_base_site + FROM {monitorings_schema}.t_site_complements AS tsc + LEFT JOIN {monitorings_schema}.cor_site_module AS csm + ON tsc.id_base_site = csm.id_base_site + WHERE csm.id_base_site IS NULL; + """ + ) + op.execute(statement) + + # Drop column id_module op.drop_column("t_site_complements", "id_module", schema=monitorings_schema) @@ -41,14 +55,19 @@ def downgrade(): ) # Cannot use orm here because need the model to be "downgraded" as well # Need to set nullable True above for existing rows - # FIXME: find a better way because need to assign a module... + # LIMITATION: Assume that current use is one site associated to one module statement = sa.text( f""" - update {monitorings_schema}.t_site_complements - set id_module = (select id_module - from gn_commons.t_modules tm - where module_code = :module_code); + WITH sm AS ( + SELECT min(id_module) AS first_id_module, id_base_site + FROM {monitorings_schema}.cor_site_module AS csm + GROUP BY id_base_site + ) + UPDATE {monitorings_schema}.t_site_complements sc + SET id_module = sm.first_id_module + FROM sm + WHERE sm.id_base_site = sc.id_base_site; """ - ).bindparams(module_code=MODULE_CODE) + ) op.execute(statement) op.alter_column("t_site_complements", "id_module", nullable=False, schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py b/backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py index 9280f92ed..bf5839dd4 100644 --- a/backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py +++ b/backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py @@ -41,13 +41,32 @@ def downgrade(): ) # Cannot use orm here because need the model to be "downgraded" as well # Need to set nullable True above for existing rows - # FIXME: find a better way because need to assign a module... + # Get data from core_site_module + # LIMITATION: Assume that current use is one site associated to one module associated to one site_group statement = sa.text( f""" - update {monitorings_schema}.t_sites_groups - set id_module = (select id_module - from gn_commons.t_modules tm - where module_code = :module_code); + WITH sgm AS ( + SELECT id_sites_group , csm.id_module + FROM gn_monitoring.t_site_complements AS tsc + JOIN gn_monitoring.cor_site_module AS csm + ON tsc.id_base_site = csm.id_base_site + WHERE NOT id_sites_group IS NULL + ) + UPDATE gn_monitoring.t_sites_groups AS tsg + SET id_module = sgm.id_module + FROM sgm + WHERE tsg.id_sites_group = sgm.id_sites_group; + """ + ) + op.execute(statement) + + statement = sa.text( + f""" + UPDATE {monitorings_schema}.t_sites_groups + SET id_module = (select id_module + from gn_commons.t_modules tm + where module_code = :module_code) + WHERE id_module IS NULL; """ ).bindparams(module_code=MODULE_CODE) op.execute(statement) From ea64abdef4023298f91434196dc5299a0b07257d Mon Sep 17 00:00:00 2001 From: amandine-sahl Date: Fri, 3 Mar 2023 15:51:03 +0100 Subject: [PATCH 025/161] Black --- .../migrations/a54bafb13ce8_create_cor_module_type.py | 2 +- .../migrations/b53bafb13ce8_create_bib_type_site.py | 1 - .../migrations/ce54ba49ce5c_create_cor_type_site.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py b/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py index c1f55ac95..7fa36fdd1 100644 --- a/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py +++ b/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py @@ -49,7 +49,7 @@ def upgrade(): op.create_table_comment( "cor_module_type", "Table d'association des types de sites potentiellement associés à un module", - schema=monitorings_schema + schema=monitorings_schema, ) diff --git a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py index c45da8c58..7e56122b2 100644 --- a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py +++ b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py @@ -63,6 +63,5 @@ def upgrade(): ) - def downgrade(): op.drop_table("bib_type_site", schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py b/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py index ea470747b..88f9fca25 100644 --- a/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py +++ b/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py @@ -48,7 +48,7 @@ def upgrade(): op.create_table_comment( "cor_type_site", "Table d'association entre les sites et les types de sites", - schema=monitorings_schema + schema=monitorings_schema, ) From c05a6e31e014afcd25084af38e6247bb25380dbd Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Fri, 13 Jan 2023 09:32:29 +0100 Subject: [PATCH 026/161] Fix/db migrations (#31) * feat(db): upgrade down_revision following rebase Since rebase with develop: changed the down_revision number * fix(db): fix bind params enabling downgrade Beforehand the downgrade was not possible... * refactor(db): removed cor_site_type_category * refactor(db): changed category into type in cor * refactor(db): create cor_type_site * fix(db): renamed column * refactor(api): update models to fit migrations * fix(db):change bib_categorie_site to bib_type_site Adding : cor_site_module cor_site_type revision alembic to create function and trigger in order to add bib_type_site but only with nomenclature 'TYPE_SITE' upgrade and downgrade works [Refs ticket]: #3 Reviewed-by: andriac * fix(api): updated models from migrations * fix(api): wip: fix admin following migrations * fix(api): update routes and tests To match migration changes * feat: flask admin bib_type_site Change bib_categories to bib_type_site into flask admin Adding filtering in list label_fr of type_site to secure the unique constraint Reviewed-by: andriac [Refs ticket]: #3 * fix(api): updated schema to match models * fix(api): module edition * style(api): uniformize type_site * style(api): change relationship name for type_site * feat(api): validator admin * fix(api): make unique BibTypeSite in admin * test(api): fix test when existing nomenclatures In database Co-authored-by: Andria Capai --- .../migrations/a54bafb13ce8_create_cor_module_type.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py b/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py index 7fa36fdd1..718194469 100644 --- a/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py +++ b/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py @@ -25,8 +25,8 @@ def upgrade(): "id_type_site", sa.Integer(), sa.ForeignKey( - f"{monitorings_schema}.bib_type_site.id_nomenclature_type_site", - name="fk_cor_module_type_id_nomenclature_type_site", + f"{monitorings_schema}.bib_type_site.id_nomenclature", + name="fk_cor_module_type_id_nomenclature", ondelete="CASCADE", onupdate="CASCADE", ), @@ -46,11 +46,6 @@ def upgrade(): sa.PrimaryKeyConstraint("id_type_site", "id_module", name="pk_cor_module_type"), schema=monitorings_schema, ) - op.create_table_comment( - "cor_module_type", - "Table d'association des types de sites potentiellement associés à un module", - schema=monitorings_schema, - ) def downgrade(): From d65f259e8a25b89188606304f9f9915448e04218 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Mon, 23 Jan 2023 16:27:42 +0100 Subject: [PATCH 027/161] Feat/get all sites (#26) * test(api): test all_geometries route * feat(api): geojson instead of geobuf for sites * feat(api): add all_sites_group_geometry route To return the geometries of all sites groups * test(api): refactor fixture To add a new one: site_group_with_sites since not all sites_groups have sites * test(api): test get_sites_groups route * feat(api): add possibility to filter On id_base_site, base_site_name and id_sites_group * test(api): add fixture to get group without site --- backend/gn_module_monitoring/routes/site.py | 22 +++++++++++++- .../routes/sites_groups.py | 30 +++++++++++++++++-- .../tests/fixtures/site.py | 4 +-- .../tests/fixtures/sites_groups.py | 10 +++++++ .../tests/test_routes/test_site.py | 28 +++++++++++++++++ .../tests/test_routes/test_sites_groups.py | 15 ++++++++++ backend/gn_module_monitoring/utils/routes.py | 20 +++++++++++++ 7 files changed, 123 insertions(+), 6 deletions(-) diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py index 92a209ad0..b4c8cc673 100644 --- a/backend/gn_module_monitoring/routes/site.py +++ b/backend/gn_module_monitoring/routes/site.py @@ -4,14 +4,15 @@ from gn_module_monitoring.blueprint import blueprint from gn_module_monitoring.monitoring.models import BibTypeSite, TMonitoringSites +from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema from gn_module_monitoring.utils.routes import ( filter_params, + geojson_query, get_limit_page, get_sort, paginate, sort, ) -from gn_module_monitoring.monitoring.schemas import MonitoringSitesSchema,BibTypeSiteSchema @blueprint.route("/sites/types", methods=["GET"]) @@ -60,6 +61,25 @@ def get_sites(): ) +@blueprint.route("/sites/geometries", methods=["GET"]) +def get_all_site_geometries(): + params = MultiDict(request.args) + subquery = ( + TMonitoringSites.query.with_entities( + TMonitoringSites.id_base_site, + TMonitoringSites.base_site_name, + TMonitoringSites.geom, + TMonitoringSites.id_sites_group, + ) + .filter_by_params(params) + .subquery() + ) + + result = geojson_query(subquery) + + return jsonify(result) + + @blueprint.route("/sites/module/", methods=["GET"]) def get_module_sites(module_code: str): # TODO: load with site_categories.json API diff --git a/backend/gn_module_monitoring/routes/sites_groups.py b/backend/gn_module_monitoring/routes/sites_groups.py index e999c2351..e04b27c65 100644 --- a/backend/gn_module_monitoring/routes/sites_groups.py +++ b/backend/gn_module_monitoring/routes/sites_groups.py @@ -1,16 +1,19 @@ -from flask import request +from flask import jsonify, request +from geonature.utils.env import db +from sqlalchemy import func from werkzeug.datastructures import MultiDict from gn_module_monitoring.blueprint import blueprint -from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups +from gn_module_monitoring.monitoring.models import TMonitoringSites, TMonitoringSitesGroups +from gn_module_monitoring.monitoring.schemas import MonitoringSitesGroupsSchema from gn_module_monitoring.utils.routes import ( filter_params, + geojson_query, get_limit_page, get_sort, paginate, sort, ) -from gn_module_monitoring.monitoring.schemas import MonitoringSitesGroupsSchema @blueprint.route("/sites_groups", methods=["GET"]) @@ -29,3 +32,24 @@ def get_sites_groups(): limit=limit, page=page, ) + + +@blueprint.route("/sites_groups/geometries", methods=["GET"]) +def get_sites_group_geometries(): + subquery = ( + db.session.query( + TMonitoringSitesGroups.id_sites_group, + TMonitoringSitesGroups.sites_group_name, + func.st_convexHull(func.st_collect(TMonitoringSites.geom)), + ) + .group_by(TMonitoringSitesGroups.id_sites_group, TMonitoringSitesGroups.sites_group_name) + .join( + TMonitoringSites, + TMonitoringSites.id_sites_group == TMonitoringSitesGroups.id_sites_group, + ) + .subquery() + ) + + result = geojson_query(subquery) + + return jsonify(result) diff --git a/backend/gn_module_monitoring/tests/fixtures/site.py b/backend/gn_module_monitoring/tests/fixtures/site.py index e3d16d973..4125b3e16 100644 --- a/backend/gn_module_monitoring/tests/fixtures/site.py +++ b/backend/gn_module_monitoring/tests/fixtures/site.py @@ -7,7 +7,7 @@ @pytest.fixture() -def sites(users, types_site, sites_groups): +def sites(users, types_site, site_group_with_sites): user = users["user"] geom_4326 = from_shape(Point(43, 24), srid=4326) sites = {} @@ -21,7 +21,7 @@ def sites(users, types_site, sites_groups): geom=geom_4326, id_nomenclature_type_site=types_site[key].id_nomenclature_type_site, types_site=[types_site[key]], - id_sites_group=sites_groups["Site_Groupe"].id_sites_group, + id_sites_group=site_group_with_sites.id_sites_group, ) with db.session.begin_nested(): db.session.add_all(sites.values()) diff --git a/backend/gn_module_monitoring/tests/fixtures/sites_groups.py b/backend/gn_module_monitoring/tests/fixtures/sites_groups.py index bfadf071e..eb86a2128 100644 --- a/backend/gn_module_monitoring/tests/fixtures/sites_groups.py +++ b/backend/gn_module_monitoring/tests/fixtures/sites_groups.py @@ -14,3 +14,13 @@ def sites_groups(): db.session.add_all(groups.values()) return groups + + +@pytest.fixture +def site_group_with_sites(sites_groups): + return sites_groups["Site_Groupe"] + + +@pytest.fixture +def site_group_without_sites(sites_groups): + return sites_groups["Site_eolien"] diff --git a/backend/gn_module_monitoring/tests/test_routes/test_site.py b/backend/gn_module_monitoring/tests/test_routes/test_site.py index 07448b1a3..bb00e5006 100644 --- a/backend/gn_module_monitoring/tests/test_routes/test_site.py +++ b/backend/gn_module_monitoring/tests/test_routes/test_site.py @@ -57,6 +57,34 @@ def test_get_sites_id_base_site(self, sites): assert len(r.json["items"]) == 1 assert r.json["items"][0]["id_base_site"] == id_base_site + def test_get_all_site_geometries(self, sites): + r = self.client.get(url_for("monitorings.get_all_site_geometries")) + + json_resp = r.json + features = json_resp.get("features") + sites_values = list(sites.values()) + assert r.content_type == "application/json" + assert json_resp.get("type") == "FeatureCollection" + assert len(features) >= len(sites_values) + for site in sites_values: + id_ = [ + obj["properties"] + for obj in features + if obj["properties"]["base_site_name"] == site.base_site_name + ][0]["id_base_site"] + assert id_ == site.id_base_site + + def test_get_all_site_geometries_filter_site_group(self, sites, site_group_without_sites): + r = self.client.get( + url_for( + "monitorings.get_all_site_geometries", + id_sites_group=site_group_without_sites.id_sites_group, + ) + ) + json_resp = r.json + features = json_resp.get("features") + assert features is None + def test_get_module_sites(self): module_code = "TEST" r = self.client.get(url_for("monitorings.get_module_sites", module_code=module_code)) diff --git a/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py b/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py index a81abf66d..f459d22ce 100644 --- a/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py +++ b/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py @@ -39,3 +39,18 @@ def test_serialize_sites_groups(self, sites_groups, sites): ).all() schema = MonitoringSitesGroupsSchema() assert [schema.dump(site) for site in groups] + + def test_get_sites_groups_geometries(self, sites, site_group_with_sites): + r = self.client.get(url_for("monitorings.get_sites_group_geometries")) + + json_resp = r.json + features = json_resp.get("features") + assert r.content_type == "application/json" + assert json_resp.get("type") == "FeatureCollection" + assert len(features) >= 1 + id_ = [ + obj["properties"] + for obj in features + if obj["properties"]["sites_group_name"] == site_group_with_sites.sites_group_name + ][0]["id_sites_group"] + assert id_ == site_group_with_sites.id_sites_group diff --git a/backend/gn_module_monitoring/utils/routes.py b/backend/gn_module_monitoring/utils/routes.py index 473f8500b..2871d86dd 100644 --- a/backend/gn_module_monitoring/utils/routes.py +++ b/backend/gn_module_monitoring/utils/routes.py @@ -2,7 +2,10 @@ from flask import Response from flask.json import jsonify +from geonature.utils.env import DB from marshmallow import Schema +from sqlalchemy import cast, func, text +from sqlalchemy.dialects.postgresql import JSON from sqlalchemy.orm import Query from werkzeug.datastructures import MultiDict @@ -37,3 +40,20 @@ def sort(query: MonitoringQuery, sort: str, sort_dir: str) -> MonitoringQuery: if sort_dir in ["desc", "asc"]: query = query.sort(label=sort, direction=sort_dir) return query + + +def geojson_query(subquery) -> bytes: + subquery_name = "q" + subquery = subquery.alias(subquery_name) + query = DB.session.query( + func.json_build_object( + text("'type'"), + text("'FeatureCollection'"), + text("'features'"), + func.json_agg(cast(func.st_asgeojson(subquery), JSON)), + ) + ) + result = query.first() + if len(result) > 0: + return result[0] + return b"" From a3d276c421823d8d5f8ad53eefdd4abcd554cdfd Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Fri, 24 Feb 2023 14:59:09 +0100 Subject: [PATCH 028/161] Feat/crud/gp sites components (#38) * feat(front): wip sites_groups component and svc * WIP feat(front): DataTable sites_groups - Table with specific data value (OK) - Table with sort column (OK) - Datatable , select row event and change color (wip). Reviewed-by: andriac [Refs ticket]: #4 * feat(front): Datatable format and selected row - Get and display data from group_site database (OK) - Selecting row and get id of row table (OK) Improve : - improve assign colname table outside the component (maybe into the class folder ?) Todo/next: - Filtering table - Add action column to table - refactor code by creating component for the ngx-datable in order to reuse component for other data Reviewed-by: andriac [Refs ticket]: #4 * feat(front): Filtering table (OK) Function filtering is working on each column Button filtering hide/display filter inputs (OK) TODO: - check for "help" icon and description for each column if description present - Check if multiple rows and pages if it's working Reviewed-by: andriac [Refs ticket]: #4 * feat(front): wip server pagination & filtering * feat(front): add sorting capability and fixes Fix: keep filters (sort and filters) on when changing pages Feat: remove useless id column Feat: add sorting * fix(db): change trigger to constraint (migrations) Delete the trigger and create check constraint on id_nomenclature column Reviewed-by: andriacap [Refs ticket]: #3 * refactor: Custom type Geojson and group properties Create custom type geojson and build properties into marshmallow schema according to this type of Geojson [Refs ticket]: #3 * refactor: site component with site-service add function to site.services.ts [Refs ticket]: #4 * refactor: create datatable component and service Separate of concern for all about table (service and component) and all about sites (service and component) reviewed_by: andriacap [Refs ticket]: #3 * fix: change offset to page change offset name to page for paginated Reviewed-by: andriacap [Refs ticket]: #4 * merge: merge interface and type merge site_group.service.ts delete site.service.ts add interfaces re arrange code from branch front-site-interface Reviewed-by: andriacap [Refs ticket]: #4 * feat: details properties sites groups Create properties components to display properties of sites_groups Adding route with id according to the site group selected Reviewed-by: andriacap [Refs ticket]: #4 * feat: display groups sites's child Display group site child into table and uder properties Use routing and id params to display property of groups site Create service site Add logic to check routing and child route to display reactive component properties and table Reviewed-by: andriacap [Refs ticket]: #4 * refactor(front): rename interfaces, remove classes Better types * feat(front): get all geometries For sites and sites_groups * feat(front): WIP: geojson service to create layers And features groups since geojson component accumulated layers without cleaning them... * feat(front): implemented select capability Need to refact a lot! * feat(api): add route to get one site_group by id * fix(front): too much / * feat(front): add get sites_group from id * fix(front): add possibility to provide Geometry To setMapData * refactor(front): sites and sites_groups component To extend a base component to gather the same methods at one place To use route children and router-outlet properly To add some rxjs operators To move some common interfaces/functions * fix(front): fix filters by adding baseFilters * feat: edit sitegroups Create edit-service Create observable to update object to edit according to form component Adapt function service of monitoring object to sites and sitesgroups WIP: - adapt remaining function of monitoring object inside form-component-g - create an object which inherit of patch/post/delete method with - think about object with ObjectType of sites_group , sites, observation ? , visit ? - reload properties or update data of properties table after update data Reviewed-by:andriacap [Refs_ticket]: #4 * feat: edit sitegroups Improving route 'patch' Adding reload properties when sitegroups is updated Refactoring EditService for more readability Reviewed-by: andriacap [Ref_ticket]: #4 * feat: improve edit Add method to define objectType according to service used (site or gpsite) Rewied-by: andriacap [Refs_ticket]: #4 * feat: improve rendering front "edit" and "add" Adding global object service to set object type and variable "add" or "edit" button Reviewed-by: andriacap [Refs_ticket]: #4 * feat: improving patch method object Adding service to share request patch,get in order to to re-use the service in form component according to the type pass inside the formcomponent Adding decorator errorhandler for blueprint routes Reviewed-by:andriacap [Refs_ticket]: #4 * feat: create site group method with form Route create with form component Using location pacakage to choose between edit form or init sitegroup (see to improve that) Rewiewed-by: andriacap [Refs ticket]: #4 * feat: delete site_group component Add route back Add site_group from front form component Reviewed-by:andriac [Refs_ticket]: #4 * chore(api): removed unused code * style(config): apply formatter * chore(front): removed unused code & console.log * feat(front): removed display map button As it was ugly * refactor(front): remove Object for keys As there is an Angular pipe to get keys and values Also removed console.log Removed unused code * style(front): reformat routes * refactor(front): add create component To isolate functionnalities * chore(front): remove unused services * chore(front): removed usused code * chore(api): remove string package And fix if that cannot be reached * chore(api): removed unused comment * chore(front): removed console.log and comments * chore(api): removed unused code and log --------- Co-authored-by: Andria Capai --- .../config/generic/config.json | 14 +- .../config/generic/sites_group.json | 15 - .../gn_module_monitoring/monitoring/models.py | 43 +- .../monitoring/schemas.py | 23 +- backend/gn_module_monitoring/routes/site.py | 3 +- .../routes/sites_groups.py | 51 +++ .../tests/test_routes/test_sites_groups.py | 10 + .../utils/errors/__init__.py | 0 .../utils/errors/errorHandler.py | 19 + .../app/class/monitoring-geom-component.ts | 30 ++ frontend/app/class/monitoring-site.ts | 13 + frontend/app/class/monitoring-sites-group.ts | 12 + .../components/modules/modules.component.html | 2 +- .../components/modules/modules.component.ts | 10 +- .../monitoring-datatable-g.component.css | 48 +++ .../monitoring-datatable-g.component.html | 121 ++++++ .../monitoring-datatable-g.component.spec.ts | 25 ++ .../monitoring-datatable-g.component.ts | 204 +++++++++ .../monitoring-form.component-g.css | 31 ++ .../monitoring-form.component-g.html | 124 ++++++ .../monitoring-form.component-g.spec.ts | 25 ++ .../monitoring-form.component-g.ts | 396 ++++++++++++++++++ .../monitoring-form.component.ts | 4 +- .../monitoring-map-list.component.css | 78 ++++ .../monitoring-map-list.component.html | 13 + .../monitoring-map-list.component.ts | 11 + .../monitoring-properties-g.component.css | 33 ++ .../monitoring-properties-g.component.html | 114 +++++ .../monitoring-properties-g.component.spec.ts | 25 ++ .../monitoring-properties-g.component.ts | 49 +++ .../monitoring-sites.component.css | 0 .../monitoring-sites.component.html | 25 ++ .../monitoring-sites.component.ts | 138 ++++++ ...onitoring-sitesgroups-create.component.css | 0 ...nitoring-sitesgroups-create.component.html | 4 + ...monitoring-sitesgroups-create.component.ts | 23 + .../monitoring-sitesgroups.component.css | 0 .../monitoring-sitesgroups.component.html | 6 + .../monitoring-sitesgroups.component.ts | 129 ++++++ frontend/app/functions/popup.ts | 14 + frontend/app/gnModule.module.ts | 86 +++- frontend/app/interfaces/column.ts | 5 + frontend/app/interfaces/form.ts | 7 + frontend/app/interfaces/geom.ts | 50 +++ frontend/app/interfaces/page.ts | 17 + frontend/app/interfaces/response.ts | 6 + frontend/app/services/api-geom.service.ts | 121 ++++++ frontend/app/services/cache.service.ts | 16 +- frontend/app/services/config.service.ts | 1 - frontend/app/services/data-table.service.ts | 90 ++++ frontend/app/services/edit-object.service.ts | 58 +++ frontend/app/services/geojson.service.ts | 112 +++++ frontend/app/services/object.service.ts | 23 + frontend/app/types/jsondata.ts | 1 + frontend/app/types/response.ts | 4 + 55 files changed, 2413 insertions(+), 69 deletions(-) create mode 100644 backend/gn_module_monitoring/utils/errors/__init__.py create mode 100644 backend/gn_module_monitoring/utils/errors/errorHandler.py create mode 100644 frontend/app/class/monitoring-geom-component.ts create mode 100644 frontend/app/class/monitoring-site.ts create mode 100644 frontend/app/class/monitoring-sites-group.ts create mode 100644 frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css create mode 100644 frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html create mode 100644 frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.spec.ts create mode 100644 frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts create mode 100644 frontend/app/components/monitoring-form-g/monitoring-form.component-g.css create mode 100644 frontend/app/components/monitoring-form-g/monitoring-form.component-g.html create mode 100644 frontend/app/components/monitoring-form-g/monitoring-form.component-g.spec.ts create mode 100644 frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts create mode 100644 frontend/app/components/monitoring-map-list/monitoring-map-list.component.css create mode 100644 frontend/app/components/monitoring-map-list/monitoring-map-list.component.html create mode 100644 frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts create mode 100644 frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.css create mode 100644 frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html create mode 100644 frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.spec.ts create mode 100644 frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts create mode 100644 frontend/app/components/monitoring-sites/monitoring-sites.component.css create mode 100644 frontend/app/components/monitoring-sites/monitoring-sites.component.html create mode 100644 frontend/app/components/monitoring-sites/monitoring-sites.component.ts create mode 100644 frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.css create mode 100644 frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.html create mode 100644 frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts create mode 100644 frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.css create mode 100644 frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.html create mode 100644 frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts create mode 100644 frontend/app/functions/popup.ts create mode 100644 frontend/app/interfaces/column.ts create mode 100644 frontend/app/interfaces/form.ts create mode 100644 frontend/app/interfaces/geom.ts create mode 100644 frontend/app/interfaces/page.ts create mode 100644 frontend/app/interfaces/response.ts create mode 100644 frontend/app/services/api-geom.service.ts create mode 100644 frontend/app/services/data-table.service.ts create mode 100644 frontend/app/services/edit-object.service.ts create mode 100644 frontend/app/services/geojson.service.ts create mode 100644 frontend/app/services/object.service.ts create mode 100644 frontend/app/types/jsondata.ts create mode 100644 frontend/app/types/response.ts diff --git a/backend/gn_module_monitoring/config/generic/config.json b/backend/gn_module_monitoring/config/generic/config.json index f2dafb345..dc748a66e 100644 --- a/backend/gn_module_monitoring/config/generic/config.json +++ b/backend/gn_module_monitoring/config/generic/config.json @@ -1,20 +1,22 @@ { "tree": { "module": { - "site": { - "visit": { - "observation": null + "sites_group": { + "site": { + "visit": { + "observation": null + } } } } }, - "synthese" : "__MODULE.B_SYNTHESE", + "synthese": "__MODULE.B_SYNTHESE", "default_display_field_names": { "user": "nom_complet", "nomenclature": "label_fr", "dataset": "dataset_name", "observer_list": "nom_liste", - "taxonomy" : "__MODULE.TAXONOMY_DISPLAY_FIELD_NAME", + "taxonomy": "__MODULE.TAXONOMY_DISPLAY_FIELD_NAME", "taxonomy_list": "nom_liste", "sites_group": "sites_group_name", "habitat": "lb_hab_fr", @@ -22,4 +24,4 @@ "municipality": "nom_com_dept", "site": "base_site_name" } -} +} \ No newline at end of file diff --git a/backend/gn_module_monitoring/config/generic/sites_group.json b/backend/gn_module_monitoring/config/generic/sites_group.json index 68bdf820d..db1f1d5ed 100644 --- a/backend/gn_module_monitoring/config/generic/sites_group.json +++ b/backend/gn_module_monitoring/config/generic/sites_group.json @@ -17,16 +17,6 @@ {"prop": "sites_group_code"} ], "generic": { - "id_sites_group": { - "type_widget": "text", - "attribut_label": "Id site", - "hidden": true - }, - "id_module": { - "type_widget": "text", - "attribut_label": "ID Module", - "hidden": true - }, "sites_group_name": { "type_widget": "text", "attribut_label": "Nom", @@ -53,11 +43,6 @@ }, "nb_visits": { "attribut_label": "Nombre de visites" - }, - "medias": { - "type_widget": "medias", - "attribut_label": "Médias", - "schema_dot_table": "gn_monitoring.t_sites_groups" } } } diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index 65160fbac..9ea917069 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -2,7 +2,7 @@ Modèles SQLAlchemy pour les modules de suivi """ from sqlalchemy import select, func, and_ -from sqlalchemy.orm import column_property +from sqlalchemy.orm import column_property, ColumnProperty, RelationshipProperty, class_mapper from sqlalchemy.dialects.postgresql import JSONB, UUID from uuid import uuid4 @@ -10,6 +10,7 @@ from utils_flask_sqla_geo.serializers import geoserializable from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.ext.declarative import declared_attr from pypnnomenclature.models import TNomenclatures, BibNomenclaturesTypes from geonature.core.gn_commons.models import TMedias @@ -21,6 +22,42 @@ from geonature.core.gn_monitoring.models import corVisitObserver from gn_module_monitoring.monitoring.queries import Query as MonitoringQuery + +class GenericModel: + @declared_attr + def __tablename__(cls): + return cls.__name__.lower() + + @classmethod + def set_id(cls) -> None: + pk_string = class_mapper(cls).primary_key[0].name + if hasattr(cls,"id_g") ==False: + pk_value= getattr(cls,pk_string) + setattr(cls,"id_g",pk_value) + + @classmethod + def get_id(cls) -> None: + pk_string = class_mapper(cls).primary_key[0].name + # print('======= ==>', pk_string) + if hasattr(cls,"id_g") ==False: + pk_value= getattr(cls,pk_string) + setattr(cls,"id_g",pk_value) + return pk_string + + @classmethod + def find_by_id(cls, _id: int) -> "GenericModel": + cls.set_id() + return cls.query.get_or_404(_id) + + @classmethod + def attribute_names(cls): + return [ + prop.key + for prop in class_mapper(cls).iterate_properties + if isinstance(prop, ColumnProperty) + ] + + cor_module_type = DB.Table( "cor_module_type", DB.Column( @@ -53,7 +90,7 @@ @serializable -class BibTypeSite(DB.Model): +class BibTypeSite(DB.Model, GenericModel): __tablename__ = "bib_type_site" __table_args__ = {"schema": "gn_monitoring"} query_class = MonitoringQuery @@ -238,7 +275,7 @@ class TMonitoringSites(TBaseSites): @serializable -class TMonitoringSitesGroups(DB.Model): +class TMonitoringSitesGroups(DB.Model, GenericModel): __tablename__ = 't_sites_groups' __table_args__ = {'schema': 'gn_monitoring'} query_class = MonitoringQuery diff --git a/backend/gn_module_monitoring/monitoring/schemas.py b/backend/gn_module_monitoring/monitoring/schemas.py index f34066c55..1816d677e 100644 --- a/backend/gn_module_monitoring/monitoring/schemas.py +++ b/backend/gn_module_monitoring/monitoring/schemas.py @@ -1,9 +1,9 @@ import json import geojson -from marshmallow import Schema, fields -from marshmallow_sqlalchemy import SQLAlchemyAutoSchema -from pypnnomenclature.schemas import NomenclatureSchema +from geonature.utils.env import MA +from marshmallow import Schema, fields, validate +from geonature.core.gn_commons.schemas import MediaSchema from gn_module_monitoring.monitoring.models import ( BibTypeSite, @@ -22,19 +22,30 @@ class PaginationSchema(Schema): return PaginationSchema -class MonitoringSitesGroupsSchema(SQLAlchemyAutoSchema): +class MonitoringSitesGroupsSchema(MA.SQLAlchemyAutoSchema): + + sites_group_name = fields.String( + validate=validate.Length(min=3,error="Length must be greater than 3"),) + class Meta: model = TMonitoringSitesGroups exclude = ("geom_geojson",) + load_instance = True + medias = MA.Nested(MediaSchema) + pk = fields.Method("set_pk",dump_only=True) geometry = fields.Method("serialize_geojson", dump_only=True) + + def set_pk(self,obj): + return self.Meta.model.get_id() def serialize_geojson(self, obj): if obj.geom_geojson is not None: return json.loads(obj.geom_geojson) + -class MonitoringSitesSchema(SQLAlchemyAutoSchema): +class MonitoringSitesSchema(MA.SQLAlchemyAutoSchema): class Meta: model = TMonitoringSites exclude = ("geom_geojson", "geom") @@ -46,7 +57,7 @@ def serialize_geojson(self, obj): return geojson.dumps(obj.as_geofeature().get("geometry")) -class BibTypeSiteSchema(SQLAlchemyAutoSchema): +class BibTypeSiteSchema(MA.SQLAlchemyAutoSchema): label = fields.Method("get_label_from_type_site") # See if useful in the future: # type_site = fields.Nested(NomenclatureSchema(only=("label_fr",)), dump_only=True) diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py index b4c8cc673..d4ee778aa 100644 --- a/backend/gn_module_monitoring/routes/site.py +++ b/backend/gn_module_monitoring/routes/site.py @@ -36,8 +36,7 @@ def get_types_site(): @blueprint.route("/sites/types/", methods=["GET"]) def get_type_site_by_id(id_type_site): - query = BibTypeSite.query.filter_by(id_nomenclature_type_site=id_type_site) - res = query.first() + res = BibTypeSite.find_by_id(id_type_site) schema = BibTypeSiteSchema() return schema.dump(res) diff --git a/backend/gn_module_monitoring/routes/sites_groups.py b/backend/gn_module_monitoring/routes/sites_groups.py index e04b27c65..8ebb8d3fd 100644 --- a/backend/gn_module_monitoring/routes/sites_groups.py +++ b/backend/gn_module_monitoring/routes/sites_groups.py @@ -1,5 +1,6 @@ from flask import jsonify, request from geonature.utils.env import db +from marshmallow import ValidationError from sqlalchemy import func from werkzeug.datastructures import MultiDict @@ -14,6 +15,7 @@ paginate, sort, ) +from gn_module_monitoring.utils.errors.errorHandler import InvalidUsage @blueprint.route("/sites_groups", methods=["GET"]) @@ -34,6 +36,13 @@ def get_sites_groups(): ) +@blueprint.route("/sites_groups/", methods=["GET"]) +def get_sites_group_by_id(id_sites_group: int): + schema = MonitoringSitesGroupsSchema() + result = TMonitoringSitesGroups.find_by_id(id_sites_group) + return jsonify(schema.dump(result)) + + @blueprint.route("/sites_groups/geometries", methods=["GET"]) def get_sites_group_geometries(): subquery = ( @@ -53,3 +62,45 @@ def get_sites_group_geometries(): result = geojson_query(subquery) return jsonify(result) + + +@blueprint.route("/sites_groups/", methods=["PATCH"]) +def patch(_id): + item_schema = MonitoringSitesGroupsSchema() + item_json = request.get_json() + item = TMonitoringSitesGroups.find_by_id(_id) + fields = TMonitoringSitesGroups.attribute_names() + for field in item_json: + if field in fields: + setattr(item, field, item_json[field]) + item_schema.load(item_json) + db.session.add(item) + + db.session.commit() + return item_schema.dump(item), 201 + + +@blueprint.route("/sites_groups/", methods=["DELETE"]) +def delete(_id): + item_schema = MonitoringSitesGroupsSchema() + item = TMonitoringSitesGroups.find_by_id(_id) + TMonitoringSitesGroups.query.filter_by(id_g=_id).delete() + db.session.commit() + return item_schema.dump(item), 201 + + +@blueprint.route("/sites_groups", methods=["POST"]) +def post(): + item_schema = MonitoringSitesGroupsSchema() + item_json = request.get_json() + item = item_schema.load(item_json) + db.session.add(item) + db.session.commit() + return item_schema.dump(item), 201 + + +@blueprint.errorhandler(ValidationError) +def handle_validation_error(error): + return InvalidUsage( + "Fields cannot be validated, message : {}".format(error.messages), status_code=422, payload=error.data + ).to_dict() diff --git a/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py b/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py index f459d22ce..0d5d4f2e3 100644 --- a/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py +++ b/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py @@ -7,6 +7,16 @@ @pytest.mark.usefixtures("client_class", "temporary_transaction") class TestSitesGroups: + def test_get_sites_group_by_id(self, sites_groups): + sites_group = list(sites_groups.values())[0] + id_sites_group = sites_group.id_sites_group + r = self.client.get( + url_for("monitorings.get_sites_group_by_id", id_sites_group=id_sites_group) + ) + + assert r.json["id_sites_group"] == id_sites_group + assert r.json["sites_group_name"] == sites_group.sites_group_name + def test_get_sites_groups(self, sites_groups): r = self.client.get(url_for("monitorings.get_sites_groups")) diff --git a/backend/gn_module_monitoring/utils/errors/__init__.py b/backend/gn_module_monitoring/utils/errors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/utils/errors/errorHandler.py b/backend/gn_module_monitoring/utils/errors/errorHandler.py new file mode 100644 index 000000000..a10b13987 --- /dev/null +++ b/backend/gn_module_monitoring/utils/errors/errorHandler.py @@ -0,0 +1,19 @@ +from geonature.utils.errors import GeonatureApiError + + +class InvalidUsage(GeonatureApiError): + status_code = 400 + + def __init__(self, message, status_code=None, payload=None): + GeonatureApiError.__init__(self, message, status_code) + self.message = message + if status_code is not None: + self.status_code = status_code + self.payload = payload + + def to_dict(self): + rv = {} + rv["payload"] = self.payload + rv["message"] = self.message + rv["status_code"] = self.status_code + return (rv, self.status_code) diff --git a/frontend/app/class/monitoring-geom-component.ts b/frontend/app/class/monitoring-geom-component.ts new file mode 100644 index 000000000..acad9b05b --- /dev/null +++ b/frontend/app/class/monitoring-geom-component.ts @@ -0,0 +1,30 @@ +import { PageInfo } from "../interfaces/page"; +import { JsonData } from "../types/jsondata"; + +const LIMIT = 10; + +type callbackFunction = (pageNumber: number, filters: JsonData) => void; + +export class MonitoringGeomComponent { + protected getAllItemsCallback: callbackFunction; + protected limit = LIMIT; + public filters = {}; + public baseFilters = {}; + + constructor() {} + + setPage(page: PageInfo) { + this.getAllItemsCallback(page.offset + 1, this.filters); + } + + setSort(filters: JsonData) { + this.filters = { ...this.baseFilters, ...filters }; + const pageNumber = 1; + this.getAllItemsCallback(pageNumber, this.filters); + } + + setFilter(filters) { + this.filters = { ...this.baseFilters, ...filters }; + this.getAllItemsCallback(1, this.filters); + } +} diff --git a/frontend/app/class/monitoring-site.ts b/frontend/app/class/monitoring-site.ts new file mode 100644 index 000000000..d33650386 --- /dev/null +++ b/frontend/app/class/monitoring-site.ts @@ -0,0 +1,13 @@ +export enum columnNameSite { + base_site_name = "Nom", + last_visit = "Dernière visite", + nb_visits = "Nb. visites", + base_site_code = "Code", + altitude_max = "Alt.max", + altitude_min = "Alt.min", +} + +export const extendedDetailsSite = { + ...columnNameSite, + base_site_description: "Description", +}; diff --git a/frontend/app/class/monitoring-sites-group.ts b/frontend/app/class/monitoring-sites-group.ts new file mode 100644 index 000000000..96fab297d --- /dev/null +++ b/frontend/app/class/monitoring-sites-group.ts @@ -0,0 +1,12 @@ +export enum columnNameSiteGroup { + sites_group_name = "Nom", + nb_sites = "Nb. sites", + nb_visits = "Nb. visites", + sites_group_code = "Code", +} + +export const extendedDetailsSiteGroup = { + ...columnNameSiteGroup, + comments: "Commentaires", + sites_group_description: "Description", +}; diff --git a/frontend/app/components/modules/modules.component.html b/frontend/app/components/modules/modules.component.html index 3519e09b6..b382502c2 100644 --- a/frontend/app/components/modules/modules.component.html +++ b/frontend/app/components/modules/modules.component.html @@ -14,7 +14,7 @@

{{titleModule}}

{{description}}
-
diff --git a/frontend/app/components/modules/modules.component.ts b/frontend/app/components/modules/modules.component.ts index a0d411dbe..febaee23b 100644 --- a/frontend/app/components/modules/modules.component.ts +++ b/frontend/app/components/modules/modules.component.ts @@ -14,8 +14,6 @@ import { AuthService, User } from "@geonature/components/auth/auth.service"; styleUrls: ['./modules.component.css'], }) export class ModulesComponent implements OnInit { - - currentUser: User; description: string; @@ -34,7 +32,7 @@ export class ModulesComponent implements OnInit { private _auth: AuthService, private _dataMonitoringObjectService: DataMonitoringObjectService, private _configService: ConfigService - ) { } + ) {} ngOnInit() { this.bLoading = true; @@ -64,10 +62,4 @@ export class ModulesComponent implements OnInit { this.currentUser["cruved"] = {}; this.currentUser["cruved_objects"] = {}; } - - onAccessSitesClick(modules) { - console.log("accès aux sites avec droits ") - console.log(modules) - } - } diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css new file mode 100644 index 000000000..6017b44ab --- /dev/null +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css @@ -0,0 +1,48 @@ +.cell-link { + cursor: pointer; +} + +:host::ng-deep .datatable-body-row.active .datatable-row-group { + background-color: rgb(117, 227, 118) !important; +} + +.link:hover { + background-color: rgba(0, 0, 0, 0.2) !important; + transition: background-color 0.5; +} + +.link { + display: inline; + transition: background-color 0.5s; + border-radius: 5px; +} + +.header-filter-span > input { + width: 100%; +} + +.header-sort-span { + /* width: 100%; */ + cursor: pointer; + text-overflow: ellipsis; + overflow: hidden; + white-space:nowrap +} + + +.header-sort-span:hover { + background-color: rgb(245, 245, 245); +} + +.icon-sort { + font-size: 1.2em; + float: right; +} + +:host::ng-deep .sort-btn { + display: none !important; +} + +.custom-dt { + box-shadow: none !important; +} diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html new file mode 100644 index 000000000..17e0a03ed --- /dev/null +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html @@ -0,0 +1,121 @@ + +
+ + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + +
+ + + help + + {{ column.name }} +
+
+ +
+
+
+ diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.spec.ts b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.spec.ts new file mode 100644 index 000000000..deaea12fe --- /dev/null +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MonitoringDatatableComponent } from './monitoring-datatable-g.component'; + +describe('MonitoringDatatableComponent', () => { + let component: MonitoringDatatableComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MonitoringDatatableComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MonitoringDatatableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts new file mode 100644 index 000000000..b152c85cd --- /dev/null +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts @@ -0,0 +1,204 @@ +import { DatatableComponent } from "@swimlane/ngx-datatable"; +import { + Component, + OnInit, + Input, + Output, + EventEmitter, + ViewChild, + SimpleChanges, + TemplateRef, +} from "@angular/core"; +import { Router, ActivatedRoute } from "@angular/router"; +import { Subject } from "rxjs"; +import { debounceTime } from "rxjs/operators"; +import { DataTableService } from "../../services/data-table.service"; +import { IColumn } from "../../interfaces/column"; +import { IPage } from "../../interfaces/page"; +import { ObjectService } from "../../services/object.service"; + +interface ItemObjectTable { + id: number | null; + selected: boolean; + visible: boolean; + current: boolean; +} +type ItemsObjectTable = { [key: string]: ItemObjectTable }; + +@Component({ + selector: "pnx-monitoring-datatable-g", + templateUrl: "./monitoring-datatable-g.component.html", + styleUrls: ["./monitoring-datatable-g.component.css"], +}) +export class MonitoringDatatableGComponent implements OnInit { + @Input() rows; + @Input() colsname: IColumn[]; + @Input() page: IPage = { count: 0, limit: 0, page: 0 }; + @Input() obj; + + @Input() rowStatus: Array; + @Output() rowStatusChange = new EventEmitter(); + + @Output() bEditChanged = new EventEmitter(); + + @Input() currentUser; + + @Output() onSort = new EventEmitter(); + @Output() onFilter = new EventEmitter(); + @Output() onSetPage = new EventEmitter(); + @Output() onDetailsRow = new EventEmitter(); + @Output() addEvent = new EventEmitter(); + + private filterSubject: Subject = new Subject(); + displayFilter: boolean = false; + objectsStatus: ItemsObjectTable; + + objectType: string = ""; + columns; + row_save; + selected = []; + filters = {}; + + @ViewChild(DatatableComponent) table: DatatableComponent; + @ViewChild("actionsTemplate") actionsTemplate: TemplateRef; + @ViewChild("hdrTpl") hdrTpl: TemplateRef; + + constructor( + private _dataTableService: DataTableService, + private _objService: ObjectService, + private router: Router, + private _Activatedroute: ActivatedRoute + ) {} + + ngOnInit() { + this.initDatatable(); + } + + initDatatable() { + // IF prefered observable compare to ngOnChanges uncomment this: + // this._dataTableService.currentCols.subscribe(newCols => { this.columns = newCols }) + this._objService.currentObjectType.subscribe((newObjType) => { + this.objectType = newObjType; + }); + + this.filters = {}; + this.filterSubject.pipe(debounceTime(500)).subscribe(() => { + this.filter(); + }); + } + + onSortEvent($event) { + this.filters = { + ...this.filters, + sort: $event.column.prop, + sort_dir: $event.newValue, + }; + this.onSort.emit(this.filters); + } + + setPage($event) { + this.onSetPage.emit($event); + } + + filterInput($event) { + this.filterSubject.next(); + } + + filter(bInitFilter = false) { + // filter all + const oldFilters = this.filters; + this.filters = Object.keys(oldFilters).reduce(function (r, e) { + if (![undefined, "", null].includes(oldFilters[e])) r[e] = oldFilters[e]; + return r; + }, {}); + this.onFilter.emit(this.filters); + } + + onSelectEvent({ selected }) { + const id = selected[0].id_group_site; + + if (!this.rowStatus) { + return; + } + + this.rowStatus.forEach((status) => { + const bCond = status.id === id; + status["selected"] = bCond && !status["selected"]; + }); + + this.setSelected(); + this.rowStatusChange.emit(this.rowStatus); + } + + setSelected() { + // this.table._internalRows permet d'avoir les ligne triées et d'avoir les bons index + + if (!this.rowStatus) { + return; + } + + const status_selected = this.rowStatus.find((status) => status.selected); + if (!status_selected) { + return; + } + + const index_row_selected = this.table._internalRows.findIndex( + (row) => row.id === status_selected.id + ); + if (index_row_selected === -1) { + return; + } + + this.selected = [this.table._internalRows[index_row_selected]]; + this.table.offset = Math.floor(index_row_selected / this.table._limit); + } + + ngOnDestroy() { + this.filterSubject.unsubscribe(); + } + + // tooltip(column) { + // return this.child0.template.fieldDefinitions[column.prop] + // ? column.name + " : " + this.child0.template.fieldDefinitions[column.prop] + // : column.name; + // } + + ngOnChanges(changes: SimpleChanges) { + // IF prefered ngOnChanges compare to observable uncomment this: + if (changes["rows"] && this.rows && this.rows.length > 0) { + this.columns = this._dataTableService.colsTable( + this.colsname, + this.rows[0] + ); + } + + if (changes["colsname"]) { + this.filters = {}; + } + + if (changes["obj"] && this.obj) { + this.objectsStatus, + (this.rowStatus = this._dataTableService.initObjectsStatus( + this.obj, + "sites_groups" + )); + } + for (const propName of Object.keys(changes)) { + switch (propName) { + case "rowStatus": + this.setSelected(); + break; + } + } + } + navigateToAddChildren(_, rowId) { + this.addEvent.emit(rowId); + this.router.navigate(["create"], { + relativeTo: this._Activatedroute, + }); + } + navigateToDetail(row) { + row["id"] = row.pk; + this.onDetailsRow.emit(row); + } +} diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.css b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.css new file mode 100644 index 000000000..a5c26b9ca --- /dev/null +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.css @@ -0,0 +1,31 @@ +:host ::ng-deep .obj-form { + margin: 0; + margin-bottom: 10px; + padding: 0; +} + +.hide-spinner { + display: none; +} + +.btn-height { + height: 39px; +} + +.float-right { +margin-left: 5px; +} + + +.float-left { + margin-right: 10px; + float: left; +} + +form:invalid { + outline: none; +} + +form.ng-invalid { + border: 0px !important; +} \ No newline at end of file diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html new file mode 100644 index 000000000..389e9284b --- /dev/null +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html @@ -0,0 +1,124 @@ + +

Attention

+

+ + Vous êtes sur le point de supprimer le groupe de site + Description du groupe de site +

+ + +
+ +
+
+ + + + + + + +
+ + + + + + + + +
+ + + + + + +
+
+
+
+
diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.spec.ts b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.spec.ts new file mode 100644 index 000000000..1cfc81be0 --- /dev/null +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MonitoringFormComponent } from './monitoring-form.component'; + +describe('MonitoringFormComponent', () => { + let component: MonitoringFormComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MonitoringFormComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MonitoringFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts new file mode 100644 index 000000000..c993c5026 --- /dev/null +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts @@ -0,0 +1,396 @@ +import { + Component, + OnInit, + Input, + Output, + EventEmitter, + SimpleChanges, +} from "@angular/core"; +import { FormGroup, FormBuilder } from "@angular/forms"; +import { MonitoringObject } from "../../class/monitoring-object"; +import { ConfigService } from "../../services/config.service"; +import { CommonService } from "@geonature_common/service/common.service"; +import { DynamicFormService } from "@geonature_common/form/dynamic-form-generator/dynamic-form.service"; +import { ActivatedRoute } from "@angular/router"; +import { EditObjectService } from "../../services/edit-object.service"; +import { Router } from "@angular/router"; +import { IDataForm } from "../../interfaces/form"; +import { ApiGeomService } from "../../services/api-geom.service"; +@Component({ + selector: "pnx-monitoring-form-g", + templateUrl: "./monitoring-form.component-g.html", + styleUrls: ["./monitoring-form.component-g.css"], +}) +export class MonitoringFormComponentG implements OnInit { + @Input() currentUser; + + @Input() objForm: FormGroup; + + // @Input() obj: any; + @Output() objChanged = new EventEmitter(); + + @Input() objectsStatus; + @Output() objectsStatusChange = new EventEmitter(); + + @Input() bEdit: boolean; + @Output() bEditChange = new EventEmitter(); + + @Input() sites: {}; + dataForm: IDataForm; + searchSite = ""; + + obj: any; + objFormsDefinition; + + meta: {}; + + public bSaveSpinner = false; + public bSaveAndAddChildrenSpinner = false; + public bDeleteSpinner = false; + public bDeleteModal = false; + public bChainInput = false; + public bAddChildren = false; + public chainShow = []; + + public queryParams = {}; + + constructor( + private _formBuilder: FormBuilder, + private _route: ActivatedRoute, + private _configService: ConfigService, + private _commonService: CommonService, + private _dynformService: DynamicFormService, + private _editService: EditObjectService, + private _apiGeomService: ApiGeomService, + private _router: Router + ) {} + + ngOnInit() { + this._editService.currentData.subscribe((dataToEdit) => { + this.obj = dataToEdit; + this.obj.bIsInitialized = true; + this._configService + .init(this.obj.moduleCode) + .pipe() + .subscribe(() => { + // return this._route.queryParamMap; + // }) + // .subscribe((queryParams) => { + this.queryParams = this._route.snapshot.queryParams || {}; + this.bChainInput = + this._configService.frontendParams()["bChainInput"]; + + const schema = this._configService.schema( + this.obj.moduleCode, + this.obj.objectType + ); + this.obj[this.obj.moduleCode] = schema; + // const schema = this.obj.schema(); + + // init objFormsDefinition + + // meta pour les parametres dynamiques + // ici pour avoir acces aux nomenclatures + this.meta = { + // nomenclatures: this._dataUtilsService.getDataUtil('nomenclature'), + // dataset: this._dataUtilsService.getDataUtil('dataset'), + // id_role: this.currentUser.id_role, + bChainInput: this.bChainInput, + parents: this.obj.parents, + }; + + this.objFormsDefinition = this._dynformService + .formDefinitionsdictToArray(schema, this.meta) + .filter((formDef) => formDef.type_widget) + .sort((a, b) => { + // medias à la fin + return a.attribut_name === "medias" + ? +1 + : b.attribut_name === "medias" + ? -1 + : 0; + }); + + // display_form pour customiser l'ordre dans le formulaire + // les éléments de display form sont placé en haut dans l'ordre du tableau + // tous les éléments non cachés restent affichés + + let displayProperties = [ + ...(this._configService.configModuleObjectParam( + this.obj.moduleCode, + this.obj.objectType, + "display_properties" + ) || []), + ]; + if (displayProperties && displayProperties.length) { + displayProperties.reverse(); + this.objFormsDefinition.sort((a, b) => { + let indexA = displayProperties.findIndex( + (e) => e == a.attribut_name + ); + let indexB = displayProperties.findIndex( + (e) => e == b.attribut_name + ); + return indexB - indexA; + }); + } + + // champs patch pour simuler un changement de valeur et déclencher le recalcul des propriété + // par exemple quand bChainInput change + this.objForm.addControl("patch_update", this._formBuilder.control(0)); + + // this._configService.configModuleObject(this.obj.moduleCode, this.obj.objectType); + // set geometry + // if (this.obj.config["geometry_type"]) { + // this.objForm.addControl( + // "geometry", + // this._formBuilder.control("", Validators.required) + // ); + // } + + // pour donner la valeur de idParent + + this.initForm(); + }); + }); + } + + /** pour réutiliser des paramètres déjà saisis */ + keepDefinitions() { + return this.objFormsDefinition.filter((def) => + this.obj.configParam("keep").includes(def.attribut_name) + ); + } + + setQueryParams() { + // par le biais des parametre query de route on donne l'id du ou des parents + // permet d'avoir un 'tree' ou un objet est sur plusieurs branches + // on attend des ids d'où test avec parseInt + for (const key of Object.keys(this.queryParams)) { + const strToInt = parseInt(this.queryParams[key]); + if (strToInt != NaN) { + this.obj.properties[key] = strToInt; + } + } + } + + /** initialise le formulaire quand le formulaire est prêt ou l'object est prêt */ + initForm() { + if (!(this.objForm && this.obj.bIsInitialized)) { + return; + } + + this.setQueryParams(); + // pour donner la valeur de l'objet au formulaire + this._editService.formValues(this.obj).subscribe((formValue) => { + this.objForm.patchValue(formValue); + this.setDefaultFormValue(); + this.dataForm = formValue; + // reset geom ? + }); + } + + keepNames() { + return this.obj.configParam("keep") || []; + } + + resetObjForm() { + // quand on enchaine les relevés + const chainShow = this.obj.configParam("chain_show"); + if (chainShow) { + this.chainShow.push( + chainShow.map((key) => this.obj.resolvedProperties[key]) + ); + this.chainShow.push(this.obj.resolvedProperties); + } + + // les valeur que l'on garde d'une saisie à l'autre + const keep = {}; + for (const key of this.keepNames()) { + keep[key] = this.obj.properties[key]; + } + + // nouvel object + this.obj = new MonitoringObject( + this.obj.moduleCode, + this.obj.objectType, + null, + this.obj.monitoringObjectService() + ); + this.obj.init({}); + + this.obj.properties[this.obj.configParam("id_field_Name")] = null; + + // pq get ????? + // this.obj.get(0).subscribe(() => { + this.obj.bIsInitialized = true; + for (const key of this.keepNames()) { + this.obj.properties[key] = keep[key]; + } + + this.objChanged.emit(this.obj); + this.objForm.patchValue({ geometry: null }); + this.initForm(); + // }); + } + + /** Pour donner des valeurs par defaut si la valeur n'est pas définie + * id_digitiser => current_user.id_role + * id_inventor => current_user.id_role + * date => today + */ + setDefaultFormValue() { + const value = this.objForm.value; + const date = new Date(); + const defaultValue = { + // id_digitiser: value["id_digitiser"] || this.currentUser.id_role, + // id_inventor: value["id_inventor"] || this.currentUser.id_role, + first_use_date: value["first_use_date"] || { + year: date.getUTCFullYear(), + month: date.getUTCMonth() + 1, + day: date.getUTCDate(), + }, + }; + this.objForm.patchValue(defaultValue); + } + + /** + * TODO faire des fonctions dans monitoring objet (ou moniotring objet service pour naviguer + */ + + /** + * Valider et renseigner les enfants + */ + navigateToAddChildren() { + this.bEditChange.emit(false); + this.obj.navigateToAddChildren(); + } + + /** + * Valider et aller à la page de l'objet + */ + navigateToDetail(id, objectType, queryParams) { + // patch bug navigation + this._router.navigate( + ["monitorings", objectType, id].filter((s) => !!s), + { + queryParams, + } + ); + this.bEditChange.emit(false); + } + + /** + * Valider et aller à la page de l'objet + */ + navigateToParent() { + this.bEditChange.emit(false); // patch bug navigation + this._router.navigateByUrl("/monitorings/sites_group"); + + // this.obj.navigateToParent(); + } + + msgToaster(action) { + // return `${action} ${this.obj.labelDu()} ${this.obj.description()} effectuée`.trim(); + return `${action} effectuée`.trim(); + } + + /** TODO améliorer site etc.. */ + onSubmit() { + const { patch_update, ...sendValue } = this.dataForm; + const action = this.obj.id + ? // ? this.obj.patch(this.objForm.value) + // : this.obj.post(this.objForm.value); + this._apiGeomService.patch(this.obj.id, sendValue) + : this._apiGeomService.create(sendValue); + const actionLabel = this.obj.id ? "Modification" : "Création"; + action.subscribe((objData) => { + this._commonService.regularToaster( + "success", + this.msgToaster(actionLabel) + ); + this.bSaveSpinner = this.bSaveAndAddChildrenSpinner = false; + this.objChanged.emit(this.obj); + + /** si c'est un module : reset de la config */ + if (this.obj.objectType === "module") { + this._configService.loadConfig(this.obj.moduleCode).subscribe(); + } + + if (this.bChainInput) { + this.resetObjForm(); + } else if (this.bAddChildren) { + this.navigateToAddChildren(); + } else { + if ( + this._configService.configModuleObjectParam( + this.obj.moduleCode, + this.obj.objectType, + "redirect_to_parent" + ) + ) { + this.navigateToParent(); + } else { + this.navigateToDetail( + this.obj.id, + this.obj.objectType, + this.queryParams + ); + } + } + }); + } + + onCancelEdit() { + if (this.obj.id) { + this.bEditChange.emit(false); + } else { + this.navigateToParent(); + } + } + + onDelete() { + this.bDeleteSpinner = true; + this._commonService.regularToaster("info", this.msgToaster("Suppression")); + // : this.obj.post(this.objForm.value); + this._apiGeomService.delete(this.obj.id).subscribe((del) => { + this.bDeleteSpinner = this.bDeleteModal = false; + this.objChanged.emit(this.obj); + setTimeout(() => { + this.navigateToParent(); + }, 100); + }); + } + + onObjFormValueChange(event) { + // let {id_module,medias, ...rest} = this.objForm.value; + // this.dataForm = rest + this.dataForm = this.objForm.value; + const change = this._configService.change( + this.obj.moduleCode, + this.obj.objectType + ); + if (!change) { + return; + } + setTimeout(() => { + change({ objForm: this.objForm, meta: this.meta }); + }, 100); + } + + procesPatchUpdateForm() { + this.objForm.patchValue({ + patch_update: this.objForm.value.patch_update + 1, + }); + } + + /** bChainInput gardé dans config service */ + bChainInputChanged() { + for (const formDef of this.objFormsDefinition) { + formDef.meta.bChainInput = this.bChainInput; + } + this._configService.setFrontendParams("bChainInput", this.bChainInput); + // patch pour recalculers + this.procesPatchUpdateForm(); + } +} diff --git a/frontend/app/components/monitoring-form/monitoring-form.component.ts b/frontend/app/components/monitoring-form/monitoring-form.component.ts index 3e0ae5634..5e3ca239a 100644 --- a/frontend/app/components/monitoring-form/monitoring-form.component.ts +++ b/frontend/app/components/monitoring-form/monitoring-form.component.ts @@ -281,13 +281,11 @@ export class MonitoringFormComponent implements OnInit { onDelete() { this.bDeleteSpinner = true; - this._commonService.regularToaster('info', this.msgToaster('Suppression')); - this.obj.delete().subscribe((objData) => { this.bDeleteSpinner = this.bDeleteModal = false; this.obj.deleted = true; this.objChanged.emit(this.obj); - + this._commonService.regularToaster('info', this.msgToaster('Suppression')); setTimeout(() => { this.navigateToParent(); }, 100); diff --git a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.css b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.css new file mode 100644 index 000000000..4c1e5a93e --- /dev/null +++ b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.css @@ -0,0 +1,78 @@ +.flex-container { + background-color: rgb(240, 240, 240); + display: flex; + } + + .flex-container > div { + width: 50%; + padding: 10px; + margin: 10px; + } + + .flex-container > div:first-child { + margin-right: 0; + } + + .scroll { + overflow-y: scroll; + } + + :host ::ng-deep .cadre { + background-color: white; + /* border: 1px solid grey;*/ + border-radius: 5px; + padding: 5px; + margin: 5px; + /* display: inline-block; */ + } + + /* TABLE */ + + .cell-link { + cursor: pointer; + } + + :host::ng-deep .datatable-body-row.active .datatable-row-group { + background-color: rgb(117, 227, 118) !important; + } + + .link:hover { + background-color: rgba(0, 0, 0, 0.2) !important; + transition: background-color 0.5; + } + + .link { + display: inline; + transition: background-color 0.5s; + border-radius: 5px; + } + + .header-filter-span > input { + width: 100%; + } + + .header-sort-span { + /* width: 100%; */ + cursor: pointer; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .header-sort-span:hover { + background-color: rgb(245, 245, 245); + } + + .icon-sort { + font-size: 1.2em; + float: right; + } + + :host::ng-deep .sort-btn { + display: none !important; + } + + .custom-dt { + box-shadow: none !important; + } + \ No newline at end of file diff --git a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html new file mode 100644 index 000000000..ee93b5280 --- /dev/null +++ b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html @@ -0,0 +1,13 @@ +
+
+
+ +
+
+
+
+ + +
+
+
\ No newline at end of file diff --git a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts new file mode 100644 index 000000000..65bf3f50d --- /dev/null +++ b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "monitoring-map-list.component", + templateUrl: "./monitoring-map-list.component.html", + styleUrls: ["./monitoring-map-list.component.css"], +}) +export class MonitoringMapListComponent { + displayMap: boolean = true; + constructor() {} +} diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.css b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.css new file mode 100644 index 000000000..c2fc53c60 --- /dev/null +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.css @@ -0,0 +1,33 @@ +table { + font-size: small; +} + +th { + text-align: right; +} + +.key { + font-weight: bold; +} + +td { + padding-left: 20px; +} + +.small-icon { + font-size: 18px; +} + +.medias-tab { + margin: 10px; +} + +.hide-spinner { + display: none; +} + + +::ng-deep .cdk-global-overlay-wrapper, +::ng-deep .cdk-overlay-container { + z-index: 99999 !important; +} diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html new file mode 100644 index 000000000..ebab4aca9 --- /dev/null +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html @@ -0,0 +1,114 @@ +
+
+ + +
+
diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.spec.ts b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.spec.ts new file mode 100644 index 000000000..dd9bf0fc3 --- /dev/null +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MonitoringPropertiesComponent } from './monitoring-properties-g.component'; + +describe('MonitoringPropertiesGComponent', () => { + let component: MonitoringPropertiesComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MonitoringPropertiesComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MonitoringPropertiesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts new file mode 100644 index 000000000..d47a3ef60 --- /dev/null +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts @@ -0,0 +1,49 @@ +import { + Component, + OnInit, + Input, + Output, + EventEmitter, + SimpleChanges, +} from "@angular/core"; +import { FormControl } from "@angular/forms"; +import { extendedDetailsSiteGroup } from "../../class/monitoring-sites-group"; +import { ISitesGroup } from "../../interfaces/geom"; +import { EditObjectService } from "../../services/edit-object.service"; +import { ObjectService } from "../../services/object.service"; + +@Component({ + selector: "pnx-monitoring-properties-g", + templateUrl: "./monitoring-properties-g.component.html", + styleUrls: ["./monitoring-properties-g.component.css"], +}) +export class MonitoringPropertiesGComponent implements OnInit { + @Input() selectedObj: ISitesGroup; + @Input() bEdit: boolean; + @Output() bEditChange = new EventEmitter(); + @Input() objectType: string; + + infosColsSiteGroups: typeof extendedDetailsSiteGroup = + extendedDetailsSiteGroup; + color: string = "white"; + dataDetails: ISitesGroup; + + datasetForm = new FormControl(); + + constructor( + private _editService: EditObjectService, + private _objService: ObjectService + ) {} + + ngOnInit() { + this._objService.currentObjectTypeParent.subscribe((newObjType) => { + this.objectType = newObjType; + }); + } + + onEditClick() { + this.bEditChange.emit(true); + this.selectedObj["id"] = this.selectedObj[this.selectedObj.pk]; + this._editService.changeDataSub(this.selectedObj); + } +} diff --git a/frontend/app/components/monitoring-sites/monitoring-sites.component.css b/frontend/app/components/monitoring-sites/monitoring-sites.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/app/components/monitoring-sites/monitoring-sites.component.html b/frontend/app/components/monitoring-sites/monitoring-sites.component.html new file mode 100644 index 000000000..4c9a60e1c --- /dev/null +++ b/frontend/app/components/monitoring-sites/monitoring-sites.component.html @@ -0,0 +1,25 @@ + + + + diff --git a/frontend/app/components/monitoring-sites/monitoring-sites.component.ts b/frontend/app/components/monitoring-sites/monitoring-sites.component.ts new file mode 100644 index 000000000..00b9cd752 --- /dev/null +++ b/frontend/app/components/monitoring-sites/monitoring-sites.component.ts @@ -0,0 +1,138 @@ +import { Component, OnInit, Input } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { forkJoin } from "rxjs"; +import { tap, map, mergeMap } from "rxjs/operators"; +import * as L from "leaflet"; +import { ISite, ISitesGroup } from "../../interfaces/geom"; +import { IPage, IPaginated } from "../../interfaces/page"; +import { columnNameSite } from "../../class/monitoring-site"; +import { MonitoringGeomComponent } from "../../class/monitoring-geom-component"; +import { setPopup } from "../../functions/popup"; +import { GeoJSONService } from "../../services/geojson.service"; +import { FormGroup, FormBuilder } from "@angular/forms"; +import { + SitesService, + SitesGroupService, +} from "../../services/api-geom.service"; +import { ObjectService } from "../../services/object.service"; + +const LIMIT = 10; + +@Component({ + selector: "monitoring-sites", + templateUrl: "./monitoring-sites.component.html", + styleUrls: ["./monitoring-sites.component.css"], +}) +export class MonitoringSitesComponent + extends MonitoringGeomComponent + implements OnInit +{ + siteGroupId: number; + sites: ISite[]; + sitesGroup: ISitesGroup; + colsName: typeof columnNameSite = columnNameSite; + page: IPage; + filters = {}; + siteGroupLayer: L.FeatureGroup; + @Input() bEdit: boolean; + objForm: FormGroup; + objectType: string; + + constructor( + private _sitesGroupService: SitesGroupService, + private _siteService: SitesService, + private _objService: ObjectService, + private router: Router, + private _Activatedroute: ActivatedRoute, + private _geojsonService: GeoJSONService, + private _formBuilder: FormBuilder + ) { + super(); + this.getAllItemsCallback = this.getSitesFromSiteGroupId; + } + + ngOnInit() { + this.objForm = this._formBuilder.group({}); + this._objService.changeObjectType(this._siteService.addObjectType()); + this.initSite(); + } + + initSite() { + this._Activatedroute.params + .pipe( + map((params) => params["id"] as number), + tap((id: number) => { + this._geojsonService.getSitesGroupsChildGeometries( + this.onEachFeatureSite(), + { id_sites_group: id } + ); + }), + mergeMap((id: number) => + forkJoin({ + sitesGroup: this._sitesGroupService.getById(id), + sites: this._sitesGroupService.getSitesChild(1, this.limit, { + id_sites_group: id, + }), + }) + ) + ) + .subscribe( + (data: { sitesGroup: ISitesGroup; sites: IPaginated }) => { + this.sitesGroup = data.sitesGroup; + this.sites = data.sites.items; + this.page = { + count: data.sites.count, + page: data.sites.page, + limit: data.sites.limit, + }; + this.siteGroupLayer = this._geojsonService.setMapData( + data.sitesGroup.geometry, + () => {} + ); + this.baseFilters = { id_sites_group: this.sitesGroup.id_sites_group }; + } + ); + } + ngOnDestroy() { + this._geojsonService.removeFeatureGroup( + this._geojsonService.sitesFeatureGroup + ); + this._geojsonService.removeFeatureGroup(this.siteGroupLayer); + } + + onEachFeatureSite() { + const baseUrl = this.router.url; + return (feature, layer) => { + const popup = setPopup( + baseUrl, + feature.properties.id_base_site, + "Site :" + feature.properties.base_site_name + ); + layer.bindPopup(popup); + }; + } + + getSitesFromSiteGroupId(page, params) { + this._sitesGroupService + .getSitesChild(page, LIMIT, params) + .subscribe((data: IPaginated) => { + this.sites = data.items; + this.page = { + count: data.count, + limit: data.limit, + page: data.page - 1, + }; + }); + } + + seeDetails($event) { + this._objService.changeObjectTypeParent(this._siteService.editObjectType()); + this.router.navigate([`sites/${$event.id_base_site}`], { + relativeTo: this._Activatedroute, + }); + } + + onObjChanged($event) { + this.initSite(); + } +} diff --git a/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.css b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.html b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.html new file mode 100644 index 000000000..69379ea77 --- /dev/null +++ b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.html @@ -0,0 +1,4 @@ + +
+ +
\ No newline at end of file diff --git a/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts new file mode 100644 index 000000000..4138a321b --- /dev/null +++ b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts @@ -0,0 +1,23 @@ +import { Component, OnInit } from "@angular/core"; +import { EditObjectService } from "../../services/edit-object.service"; +import { FormGroup, FormBuilder } from "@angular/forms"; +import { ISitesGroup } from "../../interfaces/geom"; + +@Component({ + selector: "monitoring-sitesgroups-create", + templateUrl: "./monitoring-sitesgroups-create.component.html", + styleUrls: ["./monitoring-sitesgroups-create.component.css"], +}) +export class MonitoringSitesGroupsCreateComponent implements OnInit { + siteGroup: ISitesGroup; + form: FormGroup; + constructor( + private _editService: EditObjectService, + private _formBuilder: FormBuilder + ) {} + + ngOnInit() { + this._editService.changeDataSub({}); + this.form = this._formBuilder.group({}); + } +} diff --git a/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.css b/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.html b/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.html new file mode 100644 index 000000000..2ddf3aae0 --- /dev/null +++ b/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.html @@ -0,0 +1,6 @@ + +
+ +
\ No newline at end of file diff --git a/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts b/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts new file mode 100644 index 000000000..88473a04f --- /dev/null +++ b/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts @@ -0,0 +1,129 @@ +import { Component, OnInit, Input } from "@angular/core"; +import { SitesGroupService } from "../../services/api-geom.service"; +import { columnNameSiteGroup } from "../../class/monitoring-sites-group"; +import { IPaginated, IPage } from "../../interfaces/page"; +import { Router, ActivatedRoute } from "@angular/router"; +import { columnNameSite } from "../../class/monitoring-site"; +import { ISite, ISitesGroup } from "../../interfaces/geom"; +import { GeoJSONService } from "../../services/geojson.service"; +import { MonitoringGeomComponent } from "../../class/monitoring-geom-component"; +import { setPopup } from "../../functions/popup"; +import { ObjectService } from "../../services/object.service"; +import { FormGroup, FormBuilder } from "@angular/forms"; + +const LIMIT = 10; + +@Component({ + selector: "monitoring-sitesgroups", + templateUrl: "./monitoring-sitesgroups.component.html", + styleUrls: ["./monitoring-sitesgroups.component.css"], +}) +export class MonitoringSitesGroupsComponent + extends MonitoringGeomComponent + implements OnInit +{ + @Input() page: IPage; + @Input() sitesGroups: ISitesGroup[]; + @Input() sitesChild: ISite[]; + @Input() columnNameSiteGroup: typeof columnNameSiteGroup = + columnNameSiteGroup; + @Input() columnNameSite: typeof columnNameSite = columnNameSite; + @Input() sitesGroupsSelected: ISitesGroup; + + // @Input() rows; + @Input() colsname; + @Input() obj; + objectType: string; + objForm: FormGroup; + objInitForm: Object = {}; + // siteGroupEmpty={ + // "comments" :'', + // sites_group_code: string; + // sites_group_description: string; + // sites_group_name: string; + // uuid_sites_group: string; //FIXME: see if OK + // } + + constructor( + private _sites_group_service: SitesGroupService, + public geojsonService: GeoJSONService, + private router: Router, + private _objService: ObjectService, + private _formBuilder: FormBuilder, + private _Activatedroute: ActivatedRoute // private _routingService: RoutingService + ) { + super(); + this.getAllItemsCallback = this.getSitesGroups; + } + + ngOnInit() { + this.initSiteGroup(); + } + + initSiteGroup() { + this._objService.changeObjectTypeParent( + this._sites_group_service.editObjectType() + ); + this._objService.changeObjectType( + this._sites_group_service.addObjectType() + ); + this.getSitesGroups(1); + this.geojsonService.getSitesGroupsGeometries( + this.onEachFeatureSiteGroups() + ); + } + + ngOnDestroy() { + this.geojsonService.removeFeatureGroup( + this.geojsonService.sitesGroupFeatureGroup + ); + } + + onEachFeatureSiteGroups(): Function { + const baseUrl = this.router.url; + return (feature, layer) => { + const popup = setPopup( + baseUrl, + feature.properties.id_sites_group, + "Groupe de site :" + feature.properties.sites_group_name + ); + layer.bindPopup(popup); + }; + } + + getSitesGroups(page = 1, params = {}) { + this._sites_group_service + .get(page, LIMIT, params) + .subscribe((data: IPaginated) => { + this.page = { + count: data.count, + limit: data.limit, + page: data.page - 1, + }; + this.sitesGroups = data.items; + this.colsname = this.columnNameSiteGroup; + // IF prefered observable compare to ngOnChanges uncomment this: + // this._dataTableService.changeColsTable(this.colsname,this.sitesGroups[0]) + }); + } + + seeDetails($event) { + // TODO: routerLink + this._objService.changeObjectTypeParent( + this._sites_group_service.editObjectType() + ); + this.router.navigate([$event.id_sites_group], { + relativeTo: this._Activatedroute, + }); + } + + addSiteGp($event) { + this.router.navigate(["/create"], { + relativeTo: this._Activatedroute, + }); + } + onSelect($event) { + this.geojsonService.selectSitesGroupLayer($event); + } + onObjChanged($event) {} +} diff --git a/frontend/app/functions/popup.ts b/frontend/app/functions/popup.ts new file mode 100644 index 000000000..e6e22b90c --- /dev/null +++ b/frontend/app/functions/popup.ts @@ -0,0 +1,14 @@ +export function setPopup(baseUrl: string, id: number, name: string) { + const url = `#/${baseUrl}/${id}/`; + + const popup = ` +
+

${name}

+ + + +
+ `; + + return popup; +} diff --git a/frontend/app/gnModule.module.ts b/frontend/app/gnModule.module.ts index bb54d438d..5e46a78c2 100644 --- a/frontend/app/gnModule.module.ts +++ b/frontend/app/gnModule.module.ts @@ -15,22 +15,39 @@ import { MonitoringObjectService } from './services/monitoring-object.service'; import { ConfigService } from './services/config.service'; // Component -import { BreadcrumbsComponent } from './components/breadcrumbs/breadcrumbs.component'; -import { ModulesComponent } from './components/modules/modules.component'; -import { MonitoringObjectComponent } from './components/monitoring-object/monitoring-object.component'; -import { DrawFormComponent } from './components/draw-form/draw-form.component'; -import { ModalMsgComponent } from './components/modal-msg/modal-msg.component'; -import { MonitoringMapComponent } from './components/monitoring-map/monitoring-map.component'; -import { MonitoringFormComponent } from './components/monitoring-form/monitoring-form.component'; -import { MonitoringListComponent } from './components/monitoring-lists/monitoring-lists.component'; -import { MonitoringPropertiesComponent } from './components/monitoring-properties/monitoring-properties.component'; -import { MonitoringDatatableComponent } from './components/monitoring-datatable/monitoring-datatable.component'; +import { BreadcrumbsComponent } from "./components/breadcrumbs/breadcrumbs.component"; +import { ModulesComponent } from "./components/modules/modules.component"; +import { MonitoringObjectComponent } from "./components/monitoring-object/monitoring-object.component"; +import { DrawFormComponent } from "./components/draw-form/draw-form.component"; +import { ModalMsgComponent } from "./components/modal-msg/modal-msg.component"; +import { MonitoringMapComponent } from "./components/monitoring-map/monitoring-map.component"; +import { MonitoringFormComponent } from "./components/monitoring-form/monitoring-form.component"; +import { MonitoringListComponent } from "./components/monitoring-lists/monitoring-lists.component"; +import { MonitoringPropertiesComponent } from "./components/monitoring-properties/monitoring-properties.component"; +import { MonitoringDatatableComponent } from "./components/monitoring-datatable/monitoring-datatable.component"; +import { MonitoringDatatableGComponent } from "./components/monitoring-datatable-g/monitoring-datatable-g.component"; -import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatSelectModule } from '@angular/material/select'; -import { MatInputModule } from '@angular/material/input'; +import { MatSlideToggleModule } from "@angular/material/slide-toggle"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatAutocompleteModule } from "@angular/material/autocomplete"; +import { MatSelectModule } from "@angular/material/select"; +import { MatInputModule } from "@angular/material/input"; +import { MonitoringSitesGroupsComponent } from "./components/monitoring-sitesgroups/monitoring-sitesgroups.component"; +import { DataTableService } from "./services/data-table.service"; +import { MonitoringPropertiesGComponent } from "./components/monitoring-properties-g/monitoring-properties-g.component"; +import { GeoJSONService } from "./services/geojson.service"; +import { MonitoringSitesComponent } from "./components/monitoring-sites/monitoring-sites.component"; +import { MonitoringMapListComponent } from "./components/monitoring-map-list/monitoring-map-list.component"; +import { MonitoringFormComponentG } from "./components/monitoring-form-g/monitoring-form.component-g"; +import { EditObjectService } from "./services/edit-object.service"; +import { ObjectService } from "./services/object.service"; +import { + SitesGroupService, + SitesService, + ApiGeomService, +} from "./services/api-geom.service"; +import { MonitoringVisitsComponent } from "./components/monitoring-visits/monitoring-visits.component"; +import { MonitoringSitesGroupsCreateComponent } from "./components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component"; // my module routing const routes: Routes = [ @@ -38,9 +55,9 @@ const routes: Routes = [ { path: '', component: ModulesComponent }, /** module */ - { path: 'module/:moduleCode', component: MonitoringObjectComponent }, + { path: "module/:moduleCode", component: MonitoringObjectComponent }, /** create module */ - { path: 'module', component: MonitoringObjectComponent }, + { path: "module", component: MonitoringObjectComponent }, /** object */ { @@ -52,6 +69,27 @@ const routes: Routes = [ path: 'create_object/:moduleCode/:objectType', component: MonitoringObjectComponent, }, + { + path: "sites_group", + component: MonitoringMapListComponent, + children: [ + { + path: "", + component: MonitoringSitesGroupsComponent, + }, + { path: "create", component: MonitoringSitesGroupsCreateComponent }, + { + path: ":id", + // Add new component here + children: [ + { + path: "", + component: MonitoringSitesComponent, + }, + ], + }, + ], + }, ]; @NgModule({ @@ -66,6 +104,13 @@ const routes: Routes = [ MonitoringListComponent, MonitoringPropertiesComponent, MonitoringDatatableComponent, + MonitoringMapListComponent, + MonitoringSitesGroupsComponent, + MonitoringSitesComponent, + MonitoringDatatableGComponent, + MonitoringPropertiesGComponent, + MonitoringFormComponentG, + MonitoringSitesGroupsCreateComponent, ], imports: [ GN2CommonModule, @@ -89,6 +134,13 @@ const routes: Routes = [ DataUtilsService, ConfigService, MonitoringObjectService, + DataTableService, + SitesGroupService, + SitesService, + GeoJSONService, + EditObjectService, + ObjectService, + ApiGeomService, ], bootstrap: [ModulesComponent], schemas: [ diff --git a/frontend/app/interfaces/column.ts b/frontend/app/interfaces/column.ts new file mode 100644 index 000000000..82f803037 --- /dev/null +++ b/frontend/app/interfaces/column.ts @@ -0,0 +1,5 @@ +export interface IColumn { + name: string; + prop: string; + description?: string; +} diff --git a/frontend/app/interfaces/form.ts b/frontend/app/interfaces/form.ts new file mode 100644 index 000000000..e57edf803 --- /dev/null +++ b/frontend/app/interfaces/form.ts @@ -0,0 +1,7 @@ +import { JsonData } from "../types/jsondata"; +import { ISite, ISitesGroup } from "./geom"; + +export interface IDataForm extends JsonData, ISitesGroup, ISite { + patch_update:number + } + \ No newline at end of file diff --git a/frontend/app/interfaces/geom.ts b/frontend/app/interfaces/geom.ts new file mode 100644 index 000000000..9d43b5cdf --- /dev/null +++ b/frontend/app/interfaces/geom.ts @@ -0,0 +1,50 @@ +import { GeoJSON } from "geojson"; +import { Observable } from "rxjs"; +import { JsonData } from "../types/jsondata"; +import { Resp } from "../types/response"; +import { IPaginated } from "./page"; + +export interface IGeomObject { + data: JsonData; + geometry: GeoJSON.Geometry; +} + +export interface ISitesGroup extends IGeomObject { + pk: number; + comments?: string; + id_sites_group: number; + nb_sites: number; + nb_visits: number; + sites_group_code: string; + sites_group_description: string; + sites_group_name: string; + uuid_sites_group: string; //FIXME: see if OK +} + +export interface ISite extends IGeomObject { + altitude_max: number; + altitude_min: number; + base_site_code: string; + base_site_description?: string; + base_site_name: string; + first_use_date: string; + id_base_site: number; + id_nomenclature_type_site?: number; + last_visit?: Date; + meta_create_date: Date; + meta_update_date: Date; + nb_visits: number; + uuid_base_site: string; +} + +export interface IGeomService { + get( + limit: number, + page: number, + params: JsonData + ): Observable>; + get_geometries(params: JsonData): Observable; + create(postdata: IGeomObject): Observable; + patch(id: number, updatedData: IGeomObject): Observable; + // delete(obj: IGeomObject) +} diff --git a/frontend/app/interfaces/page.ts b/frontend/app/interfaces/page.ts new file mode 100644 index 000000000..cca644c5f --- /dev/null +++ b/frontend/app/interfaces/page.ts @@ -0,0 +1,17 @@ +export interface IPage { + count: number; + limit: number; + page: number; +} + +export interface IPaginated extends IPage { + items: T[]; +} + +// PageInfo = object given by ngx-datatable +export interface PageInfo { + offset: number; + pageSize: number; + limit: number; + count: number; +} diff --git a/frontend/app/interfaces/response.ts b/frontend/app/interfaces/response.ts new file mode 100644 index 000000000..317f5c94d --- /dev/null +++ b/frontend/app/interfaces/response.ts @@ -0,0 +1,6 @@ +import { JsonData } from "../types/jsondata"; +export interface ResponseUpdated { + message: string; + payload: JsonData; + status_code: number; +} diff --git a/frontend/app/services/api-geom.service.ts b/frontend/app/services/api-geom.service.ts new file mode 100644 index 000000000..296913ebf --- /dev/null +++ b/frontend/app/services/api-geom.service.ts @@ -0,0 +1,121 @@ +import { Injectable } from "@angular/core"; +import { Observable } from "rxjs"; +import { GeoJSON } from "geojson"; + +import { CacheService } from "./cache.service"; +import { IGeomService, ISitesGroup, ISite } from "../interfaces/geom"; +import { IPaginated } from "../interfaces/page"; +import { JsonData } from "../types/jsondata"; +import { Resp } from "../types/response"; + +export enum endPoints { + sites_groups = "sites_groups", + sites = "sites", +} + +@Injectable() +export class ApiGeomService implements IGeomService { + public objectType: endPoints = endPoints.sites_groups; + + constructor(protected _cacheService: CacheService) { + this.init(); + } + + init() { + this.objectType = endPoints.sites_groups; + } + get( + page: number = 1, + limit: number = 10, + params: JsonData = {} + ): Observable> { + return this._cacheService.request< + Observable> + >("get", this.objectType, { + queryParams: { page, limit, ...params }, + }); + } + + getById(id: number): Observable { + return this._cacheService.request>( + "get", + `${this.objectType}/${id}` + ); + } + + get_geometries(params: JsonData = {}): Observable { + return this._cacheService.request>( + "get", + `${this.objectType}/geometries`, + { + queryParams: { ...params }, + } + ); + } + + patch(id: number, updatedData: ISitesGroup | ISite): Observable { + return this._cacheService.request("patch", `${this.objectType}/${id}`, { + postData: updatedData, + }); + } + + create( postData: ISitesGroup | ISite): Observable { + return this._cacheService.request("post", `${this.objectType}`, { + postData: postData, + }); + } + + delete(id: number): Observable { + return this._cacheService.request("delete", `${this.objectType}/${id}`); + } + +} + +@Injectable() +export class SitesGroupService extends ApiGeomService { + constructor(_cacheService: CacheService) { + super(_cacheService); + } + init(): void { + this.objectType = endPoints.sites_groups; + } + + getSitesChild( + page: number = 1, + limit: number = 10, + params: JsonData = {} + ): Observable> { + return this._cacheService.request>>( + "get", + `sites`, + { + queryParams: { page, limit, ...params }, + } + ); + } + + addObjectType(): string { + return "un nouveau groupe de site"; + } + + editObjectType(): string { + return "le groupe de site"; + } +} + +@Injectable() +export class SitesService extends ApiGeomService { + constructor(_cacheService: CacheService) { + super(_cacheService); + } + init(): void { + this.objectType = endPoints.sites; + } + addObjectType(): string { + return " un nouveau site"; + } + + editObjectType(): string { + return "le site"; + } +} diff --git a/frontend/app/services/cache.service.ts b/frontend/app/services/cache.service.ts index f8d0ed5e7..952d62d47 100644 --- a/frontend/app/services/cache.service.ts +++ b/frontend/app/services/cache.service.ts @@ -2,8 +2,8 @@ import { ObserversComponent } from '@geonature_common/form/observers/observers.c import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Observable, of, Subject } from 'rxjs'; -import { mergeMap, concatMap } from 'rxjs/operators'; +import { Observable, of, Subject } from "rxjs"; +import { mergeMap} from "rxjs/operators"; import { ConfigService } from './config.service'; @@ -30,10 +30,14 @@ export class CacheService { * @param urlRelative url relative de la route * @param data post data (optionnel) */ - request(requestType: string, urlRelative: string, { postData = {}, queryParams = {} } = {}) { + request>( + requestType: string, + urlRelative: string, + { postData = {}, queryParams = {} } = {} + ): Return { // verification de requestType - if (!['get', 'post', 'patch', 'delete'].includes(requestType)) { - return of(null); + if (!["get", "post", "patch", "delete"].includes(requestType)) { + throw console.error("Request must be get, post, patch or delete"); } const url_params = Object.keys(queryParams).length @@ -50,7 +54,7 @@ export class CacheService { const url = this._config.backendModuleUrl() + '/' + urlRelative + url_params; // requete - return this._http[requestType](url, postData); + return this._http[requestType](url, postData); } /** Cache diff --git a/frontend/app/services/config.service.ts b/frontend/app/services/config.service.ts index c85ef94fd..15f5c4ec9 100644 --- a/frontend/app/services/config.service.ts +++ b/frontend/app/services/config.service.ts @@ -141,7 +141,6 @@ export class ConfigService { moduleCode = moduleCode || 'generic'; const configObject = this._config[moduleCode][objectType]; - // gerer quand les paramètres ont un fonction comme valeur for (const typeSchema of ['generic', 'specific']) { diff --git a/frontend/app/services/data-table.service.ts b/frontend/app/services/data-table.service.ts new file mode 100644 index 000000000..8a76099ee --- /dev/null +++ b/frontend/app/services/data-table.service.ts @@ -0,0 +1,90 @@ +import { Injectable } from "@angular/core"; +import { IColumn } from "../interfaces/column"; +import { BehaviorSubject} from "rxjs"; +interface ItemObjectTable { + id: number | null; + selected: boolean; + visible: boolean; + current: boolean; +} + +type ItemsObjectTable = { [key: string]: ItemObjectTable }; + +@Injectable() +export class DataTableService { + obj: ItemsObjectTable; + objectsStatus: ItemsObjectTable; + rowStatus: ItemObjectTable; + idObj: number; + + + // IF prefered observable compare to ngOnChanges uncomment this: + // dataCol:IColumn[] =[{prop:"",name:"",description:""}] + // private dataCols = new BehaviorSubject(this.dataCol); + // currentCols = this.dataCols.asObservable(); + + + constructor() {} + + // IF prefered observable compare to ngOnChanges uncomment this: + // changeColsTable(newCols:IColumn[],newRows){ + // const arr = Object.keys(newCols); + // const allColumn: IColumn[] = arr + // .filter((item) => Object.keys(newRows).includes(item)) + // .map((elm) => ({ + // name: newCols[elm], + // prop: elm, + // description: elm, + // })); + // this.dataCols.next(allColumn) + // } + + colsTable(colName:IColumn[], dataTable): IColumn[] { + const arr = Object.keys(colName); + const allColumn: IColumn[] = arr + .filter((item) => Object.keys(dataTable).includes(item)) + .map((elm) => ({ + name: colName[elm], + prop: elm, + description: elm, + })); + return allColumn; + } + + initObjectsStatus(obj, key) { + const objectsStatus = {}; + // for (const childrenType of Object.keys(this.obj.children)) { + objectsStatus[key] = obj.map((groupSite) => { + return { + id: groupSite.id_sites_group, + selected: false, + visible: true, + current: false, + }; + }); + // } + + // init site status + if (this.idObj) { + objectsStatus[key] = []; + obj.features.forEach((f) => { + // determination du site courrant + let cur = false; + if (f.properties.id_sites_group == this.idObj) { + cur = true; + } + + objectsStatus[key].push({ + id: f.properties.id_sites_group, + selected: false, + visible: true, + current: cur, + }); + }); + } + + this.objectsStatus = objectsStatus; + this.rowStatus = this.objectsStatus[key]; + return [this.objectsStatus, this.rowStatus]; + } +} diff --git a/frontend/app/services/edit-object.service.ts b/frontend/app/services/edit-object.service.ts new file mode 100644 index 000000000..fc106678c --- /dev/null +++ b/frontend/app/services/edit-object.service.ts @@ -0,0 +1,58 @@ +import { Injectable } from "@angular/core"; +import { BehaviorSubject, Observable, of, forkJoin } from "rxjs"; +import { concatMap } from "rxjs/operators"; + +import { JsonData } from "../types/jsondata"; + +import { Utils } from "../utils/utils"; +import { MonitoringObjectService } from "./monitoring-object.service"; + +@Injectable() +export class EditObjectService { + data: JsonData = {}; + private dataSub = new BehaviorSubject(this.data); + currentData = this.dataSub.asObservable(); + properties: JsonData; + moduleCode:string; + objecType:string; + + constructor( + private _objService:MonitoringObjectService + ) {} + + changeDataSub(newDat: JsonData) { + this.properties = newDat; + newDat.moduleCode = "generic"; + newDat.objectType = "sites_group"; + this.moduleCode= "generic"; + this.objecType= "sites_group" + this.dataSub.next(newDat) + + } + + + + formValues(obj): Observable { + const properties = Utils.copy(this.properties); + const observables = {}; + const schema = obj[this.moduleCode]; + for (const attribut_name of Object.keys(schema)) { + const elem = schema[attribut_name]; + if (!elem.type_widget) { + continue; + } + observables[attribut_name] = this._objService.toForm(elem, properties[attribut_name]); + } + + return forkJoin(observables).pipe( + concatMap((formValues_in) => { + const formValues = Utils.copy(formValues_in); + // geometry + // if (this.config["geometry_type"]) { + // formValues["geometry"] = this.geometry; // copy??? + // } + return of(formValues); + }) + ); + } +} diff --git a/frontend/app/services/geojson.service.ts b/frontend/app/services/geojson.service.ts new file mode 100644 index 000000000..22a26e980 --- /dev/null +++ b/frontend/app/services/geojson.service.ts @@ -0,0 +1,112 @@ +import { Injectable } from "@angular/core"; +import * as L from "leaflet"; +import { Observable } from "rxjs"; +import { map } from "rxjs/operators"; +import { GeoJSON } from "geojson"; +import { MapService } from "@geonature_common/map/map.service"; + +import { SitesService,SitesGroupService } from "./api-geom.service"; + + +// This service will be used for sites and sites groups + +const siteGroupStyle = { + fillColor: "#800080", + fillOpacity: 0.5, + color: "#800080", + opacity: 0.8, + weight: 2, + fill: true, +}; + +@Injectable() +export class GeoJSONService { + geojsonSitesGroups: GeoJSON.FeatureCollection; + geojsonSites: GeoJSON.FeatureCollection; + sitesGroupFeatureGroup: L.FeatureGroup; + sitesFeatureGroup: L.FeatureGroup; + + constructor( + private _sites_group_service: SitesGroupService, + private _sites_service: SitesService, + private _mapService: MapService + ) {} + + getSitesGroupsGeometries(onEachFeature: Function, params = {}) { + this._sites_group_service + .get_geometries(params) + .subscribe((data: GeoJSON.FeatureCollection) => { + this.geojsonSitesGroups = data; + this.sitesGroupFeatureGroup = this.setMapData( + data, + onEachFeature, + siteGroupStyle + ); + }); + } + + getSitesGroupsChildGeometries(onEachFeature: Function, params = {}) { + this._sites_service + .get_geometries(params) + .subscribe((data: GeoJSON.FeatureCollection) => { + //this.removeFeatureGroup(this.sitesFeatureGroup); + this.sitesFeatureGroup = this.setMapData(data, onEachFeature); + }); + } + + setMapData( + geojson: GeoJSON.Geometry | GeoJSON.FeatureCollection, + onEachFeature: Function, + style? + ) { + const map = this._mapService.getMap(); + const layer: L.Layer = this._mapService.createGeojson( + geojson, + false, + onEachFeature, + style + ); + const featureGroup = new L.FeatureGroup(); + this._mapService.map.addLayer(featureGroup); + featureGroup.addLayer(layer); + map.fitBounds(featureGroup.getBounds()); + return featureGroup; + } + + removeFeatureGroup(feature: L.FeatureGroup) { + if (feature) { + this._mapService.map.removeLayer(feature); + } + } + + onEachFeature() {} + + filterSitesGroups(siteGroupId: number) { + if (this.geojsonSitesGroups !== undefined) { + const features = this.geojsonSitesGroups.features.filter( + (feature) => feature.properties.id_sites_group == siteGroupId + ); + this.geojsonSitesGroups.features = features; + this.removeFeatureGroup(this.sitesGroupFeatureGroup); + this.setMapData( + this.geojsonSitesGroups, + this.onEachFeature, + siteGroupStyle + ); + } + } + + selectSitesGroupLayer(id: number) { + this.sitesGroupFeatureGroup.eachLayer((layer) => { + if (layer instanceof L.GeoJSON) { + layer.eachLayer((sublayer: L.GeoJSON) => { + const feature = sublayer.feature as GeoJSON.Feature; + if (feature.properties["id_sites_group"] == id) { + sublayer.openPopup(); + return; + } + }); + } + }); + } +} diff --git a/frontend/app/services/object.service.ts b/frontend/app/services/object.service.ts new file mode 100644 index 000000000..c570f506c --- /dev/null +++ b/frontend/app/services/object.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from "@angular/core"; +import { BehaviorSubject } from "rxjs"; + +@Injectable() +export class ObjectService { + objectType: string = ""; + private dataObjType = new BehaviorSubject(this.objectType); + currentObjectType = this.dataObjType.asObservable(); + + objectTypeParent: string = ""; + private dataObjTypeParent = new BehaviorSubject(this.objectTypeParent); + currentObjectTypeParent = this.dataObjTypeParent.asObservable(); + + constructor() {} + + changeObjectType(newObjType: string) { + this.dataObjType.next(newObjType); + } + + changeObjectTypeParent(newObjType: string) { + this.dataObjTypeParent.next(newObjType); + } +} diff --git a/frontend/app/types/jsondata.ts b/frontend/app/types/jsondata.ts new file mode 100644 index 000000000..fa13d6798 --- /dev/null +++ b/frontend/app/types/jsondata.ts @@ -0,0 +1 @@ +export type JsonData = { [key: string]: any }; diff --git a/frontend/app/types/response.ts b/frontend/app/types/response.ts new file mode 100644 index 000000000..dd26b3360 --- /dev/null +++ b/frontend/app/types/response.ts @@ -0,0 +1,4 @@ +import { ISite, ISitesGroup } from "../interfaces/geom"; +import { ResponseUpdated } from "../interfaces/response"; + +export type Resp = ResponseUpdated | ISite | ISitesGroup; From 0da7e35f2e258eb96fad6f4217305bbb7b1f072a Mon Sep 17 00:00:00 2001 From: andriacap <111564663+andriacap@users.noreply.github.com> Date: Thu, 2 Mar 2023 14:36:39 +0100 Subject: [PATCH 029/161] fix: remove no existing function and component visit (#39) Hotfix because no compilable with no existing component and no existing function Reviewed-by: andriacap --- .../components/monitoring-sites/monitoring-sites.component.html | 1 - frontend/app/gnModule.module.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/frontend/app/components/monitoring-sites/monitoring-sites.component.html b/frontend/app/components/monitoring-sites/monitoring-sites.component.html index 4c9a60e1c..3294f13b8 100644 --- a/frontend/app/components/monitoring-sites/monitoring-sites.component.html +++ b/frontend/app/components/monitoring-sites/monitoring-sites.component.html @@ -20,6 +20,5 @@ (onFilter)="setFilter($event)" (onSetPage)="setPage($event)" [obj]="sites" - (rowStatusChange)="onSelect($event)" (onDetailsRow)="seeDetails($event)" > diff --git a/frontend/app/gnModule.module.ts b/frontend/app/gnModule.module.ts index 5e46a78c2..69016aefd 100644 --- a/frontend/app/gnModule.module.ts +++ b/frontend/app/gnModule.module.ts @@ -46,7 +46,6 @@ import { SitesService, ApiGeomService, } from "./services/api-geom.service"; -import { MonitoringVisitsComponent } from "./components/monitoring-visits/monitoring-visits.component"; import { MonitoringSitesGroupsCreateComponent } from "./components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component"; // my module routing From 20d5f32db8938f542c3eef5b8ba9e46d687f384e Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Fri, 14 Apr 2023 14:19:10 +0200 Subject: [PATCH 030/161] Feat/dynamic form/site (#42) * refactor: object.service with observable obj Change the objectType string to objectType obj in order to subscribe to multilple properties link to this object Reviewed-by: andriacap [Refs_ticket]: #40 * feat: adapt service to get config json Get fieldsName and fieldsLabel for display properties (properties component) Add logic to setItem in LocalStorage to keepLast Value of observable on reload page TODO: - [ ] see if Localstorage is really necessary with the configService used inside api-geom.service ) - [ ] adapt monitoring-form-g.component with the new service Reviewed-by: andriacap [Refs ticket]: #4 * feat: column datatable from config json Add logic into sitegroups service in order to use config json to diplay column datatable (called "display_list") Review-by: andriac [Refs ticket]: #4 * feat: adjust backend to load config json Adjust backend code to use existing code in order to load config from file json and by starting with "sites_group" Fix Media load and upload for site_group TODO: - [ ] check if config should be find from file or backend - [ ] Optimize logic backend (use generic model with class method ?) Reviewed-by: andriacap [Refs ticket]: #4 * feat: add button multiselect with filter on backend filter with params backend (new route -> see if it's possible to change that) Add button multiselect above form type site TODO: - improve Input / condition of use case of btn multiselect Reviewed-by: andriac * feat: btn multiselect option -Add @Input fn , placeholder, title, paramsToFilt -Remove empty option -prevent adding from keyboard input or not including in list -Store config json into object Reviewed-by: @andriac [Refs-ticket]: #4 * refactor: change form-service and object-service refactor form-service add observable into object-service (WIP: futur use for refresh page ?) Rieviewed-by: andriac [Refs_ticket]: #40 * refactor: test refresh page and comportment obs refresh page seems to works with localstorage of different obj in object_service Reviewed-by: andriac * feat: dynamic form - Add: - pass config json to form.component-g.ts - add config json to this.obj and refresh form into form.component-g.ts - add css for form component to deal when long form selected -fix : - refresh page form component (this._configService.init is necessary ...) - comportment different between add or edit into form component and form service Reviewed-by: andriac [Refs_ticket]: #4 * feat: dynamic form - Correction PR Remove unused console.log Rxjs : use concatMap to avoid subscribe inside subscribe Apply: prettier and sort prettier for import ts file Reviewed-by: @andriac [Refs_PR]: #42 * feat: dynamic create site - Add change current-object when click "Add " from datatable component - api-geom.service: change the way to init the api-geom - WIP: two subscribes in one .. (the subscription is call only once ..) - WIP : get config json from backend Reviewed-by: andriac * feat: add relationship id into sendData form site Add type_site and id_site_group to the form site Reviewed-by: andriac [Refs_ticket]: #5 et #6 * feat(back): add custom config when post site Change backend files in order to choose specific config when site is created. Reviewed-by: andriac [Refs_ticket]: #5 , #6 * fix: problem of type object when loading form Add endPoint and objectType to the observable using by the formservice in order to use _apiGeomService with good context (endPoint) [Refs_ticket]: #5 et #6 * fix: todo comments about refactoring code Add todo comment in order to don't forget what is to improve inside the code * chore(front): add comment and remove console.log * chore(api): put back old formatting * chore(api): remove useless comment * refactor(api): move and rename create_update fct * fix: put back inventor/digitiser & fix typo * chore: remove useless files Since columns are now loaded from json, they are not needed anymore in separate typescript files * chore: remove definition from sites_group.json * fix: column problem on sites * chore: remove useless comment * refactor(front): remove comment and use rxjs * chore(front): remove useless file Since class BtnMultiSelectChipClass is not used anymore * chore(front): remove useless comments/imports * style(front): applied prettier * chore(front): remove useless code * refactor(front): regroup types into an interface * style(front): applied prettier * refactor(front): inherit config-json from config --------- Co-authored-by: Andria Capai --- .../config/generic/site.json | 41 +-- .../config/generic/sites_group.json | 5 + .../config/repositories.py | 48 +-- .../gn_module_monitoring/monitoring/base.py | 2 +- .../monitoring/definitions.py | 25 +- .../monitoring/objects.py | 13 +- .../monitoring/schemas.py | 2 +- .../monitoring/serializer.py | 30 +- backend/gn_module_monitoring/routes/site.py | 41 ++- .../routes/sites_groups.py | 39 ++- backend/gn_module_monitoring/utils/routes.py | 46 ++- frontend/app/class/monitoring-site.ts | 13 - frontend/app/class/monitoring-sites-group.ts | 12 - .../btn-select/btn-select.component.css | 5 + .../btn-select/btn-select.component.html | 31 ++ .../btn-select/btn-select.component.ts | 123 ++++++++ .../monitoring-datatable-g.component.css | 37 ++- .../monitoring-datatable-g.component.html | 28 +- .../monitoring-datatable-g.component.spec.ts | 7 +- .../monitoring-datatable-g.component.ts | 66 ++-- .../monitoring-form.component-g.css | 21 +- .../monitoring-form.component-g.html | 59 ++-- .../monitoring-form.component-g.spec.ts | 7 +- .../monitoring-form.component-g.ts | 289 +++++++++--------- .../monitoring-properties-g.component.html | 50 ++- .../monitoring-properties-g.component.ts | 42 +-- .../monitoring-sites-create.component.css | 0 .../monitoring-sites-create.component.html | 13 + .../monitoring-sites-create.component.ts | 82 +++++ .../monitoring-sites.component.html | 3 +- .../monitoring-sites.component.ts | 17 +- ...monitoring-sitesgroups-create.component.ts | 12 +- .../monitoring-sitesgroups.component.ts | 22 +- frontend/app/enum/endpoints.ts | 5 + frontend/app/gnModule.module.ts | 27 +- frontend/app/interfaces/geom.ts | 33 +- frontend/app/interfaces/objObs.ts | 24 ++ frontend/app/services/api-geom.service.ts | 239 +++++++++++---- frontend/app/services/config-json.service.ts | 60 ++++ frontend/app/services/config.service.ts | 28 +- frontend/app/services/data-table.service.ts | 11 +- frontend/app/services/edit-object.service.ts | 58 ---- frontend/app/services/form.service.ts | 105 +++++++ frontend/app/services/object.service.ts | 52 +++- frontend/app/utils/utils.ts | 8 + 45 files changed, 1266 insertions(+), 615 deletions(-) delete mode 100644 frontend/app/class/monitoring-site.ts delete mode 100644 frontend/app/class/monitoring-sites-group.ts create mode 100644 frontend/app/components/btn-select/btn-select.component.css create mode 100644 frontend/app/components/btn-select/btn-select.component.html create mode 100644 frontend/app/components/btn-select/btn-select.component.ts create mode 100644 frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.css create mode 100644 frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.html create mode 100644 frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.ts create mode 100644 frontend/app/enum/endpoints.ts create mode 100644 frontend/app/interfaces/objObs.ts create mode 100644 frontend/app/services/config-json.service.ts delete mode 100644 frontend/app/services/edit-object.service.ts create mode 100644 frontend/app/services/form.service.ts diff --git a/backend/gn_module_monitoring/config/generic/site.json b/backend/gn_module_monitoring/config/generic/site.json index da4956690..01c741d1f 100644 --- a/backend/gn_module_monitoring/config/generic/site.json +++ b/backend/gn_module_monitoring/config/generic/site.json @@ -11,7 +11,6 @@ "base_site_name", "base_site_code", "base_site_description", - "id_nomenclature_type_site", "id_inventor", "first_use_date", "last_visit", @@ -35,11 +34,6 @@ "attribut_label": "Id site", "hidden": true }, - "id_module": { - "type_widget": "text", - "attribut_label": "ID Module", - "hidden": true - }, "base_site_code": { "type_widget": "text", "attribut_label": "Code", @@ -54,29 +48,6 @@ "type_widget": "textarea", "attribut_label": "Description" }, - - "id_sites_group": { - "type_widget": "datalist", - "attribut_label": "Groupe de sites", - "type_util": "sites_group", - "keyValue": "id_sites_group", - "keyLabel": "sites_group_name", - "api": "__MONITORINGS_PATH/list/__MODULE.MODULE_CODE/sites_group?id_module=__MODULE.ID_MODULE&fields=id_sites_group&fields=sites_group_name", - "application": "GeoNature", - "required": false, - "hidden": true - }, - "id_nomenclature_type_site": { - "type_widget": "datalist", - "attribut_label": "Type site", - "api": "nomenclatures/nomenclature/TYPE_SITE", - "application": "GeoNature", - "keyValue": "id_nomenclature", - "keyLabel": "label_fr", - "data_path": "values", - "type_util": "nomenclature", - "required": true - }, "id_inventor": { "type_widget": "datalist", "attribut_label": "Descripteur", @@ -121,6 +92,18 @@ "altitude_max": { "type_widget": "integer", "attribut_label": "Altitude (max)" + }, + "id_sites_group": { + "type_widget": "integer", + "attribut_label": "ID Sites Groups", + "hidden": true, + "schema_dot_table": "gn_monitoring.t_base_sites" + }, + "types_site": { + "type_widget": "datalist", + "attribut_label": "ID Type sites", + "hidden": true, + "schema_dot_table": "gn_monitoring.t_base_sites" } } } diff --git a/backend/gn_module_monitoring/config/generic/sites_group.json b/backend/gn_module_monitoring/config/generic/sites_group.json index db1f1d5ed..0fbd0aea6 100644 --- a/backend/gn_module_monitoring/config/generic/sites_group.json +++ b/backend/gn_module_monitoring/config/generic/sites_group.json @@ -43,6 +43,11 @@ }, "nb_visits": { "attribut_label": "Nombre de visites" + }, + "medias": { + "type_widget": "medias", + "attribut_label": "Médias", + "schema_dot_table": "gn_monitoring.t_sites_groups" } } } diff --git a/backend/gn_module_monitoring/config/repositories.py b/backend/gn_module_monitoring/config/repositories.py index f196ebede..8886ff3ac 100644 --- a/backend/gn_module_monitoring/config/repositories.py +++ b/backend/gn_module_monitoring/config/repositories.py @@ -25,10 +25,10 @@ config_cache_name = "MONITORINGS_CONFIG" -def get_config_objects(module_code, config, tree=None, parent_type=None): - """ - recupere la config de chaque object present dans tree pour le module - """ +def get_config_objects(module_code, config, tree=None, parent_type=None, customSpecConfig=None): + ''' + recupere la config de chaque object present dans tree pour le module + ''' if not tree: # initial tree tree = config["tree"] @@ -36,7 +36,10 @@ def get_config_objects(module_code, config, tree=None, parent_type=None): for object_type in tree: # config object if not object_type in config: - config[object_type] = config_object_from_files(module_code, object_type) + if (object_type=="site"): + config[object_type] = config_object_from_files(module_code, object_type,customSpecConfig) + else: + config[object_type] = config_object_from_files(module_code, object_type) # tree children_types = tree[object_type] and list(tree[object_type].keys()) or [] @@ -77,25 +80,25 @@ def get_config_objects(module_code, config, tree=None, parent_type=None): # recursif if tree[object_type]: - get_config_objects(module_code, config, tree[object_type], object_type) + get_config_objects(module_code, config, tree[object_type], object_type,customSpecConfig) -def config_object_from_files(module_code, object_type): - """ - recupere la configuration d'un object de type pour le module - """ - generic_config_object = json_config_from_file("generic", object_type) - specific_config_object = ( - {} if module_code == "generic" else json_config_from_file(module_code, object_type) - ) +def config_object_from_files(module_code, object_type,custom=None): + ''' + recupere la configuration d'un object de type pour le module + ''' + generic_config_object = json_config_from_file('generic', object_type) + specific_config_object = {} if module_code == 'generic' else json_config_from_file(module_code, object_type) + if module_code == 'generic' and object_type == 'site' and custom is not None: + specific_config_object = custom config_object = generic_config_object config_object.update(specific_config_object) return config_object -def get_config(module_code=None, force=False): +def get_config(module_code=None, force=False,customSpecConfig=None): """ recupere la configuration pour le module monitoring @@ -133,8 +136,9 @@ def get_config(module_code=None, force=False): # if config and config.get('last_modif', 0) >= last_modif: # return config - config = config_from_files("config", module_code) - get_config_objects(module_code, config) + config = config_from_files('config', module_code) + get_config_objects(module_code, config,customSpecConfig=customSpecConfig) + # customize config if module: custom = {} @@ -242,3 +246,13 @@ def config_schema(module_code, object_type, type_schema="all"): def get_config_frontend(module_code=None, force=True): config = dict(get_config(module_code, force)) return config + + + +# def get_config_from_backend(module_code=None, force=False): + +# module_code = 'generic' +# #TODO: voir la sortie de cette fonction +# config = config_from_backend('config', module_code) +# #TODO: voir également à quoi sert cette fonction +# get_config_objects(module_code, config) diff --git a/backend/gn_module_monitoring/monitoring/base.py b/backend/gn_module_monitoring/monitoring/base.py index ab331e005..e4a263c87 100644 --- a/backend/gn_module_monitoring/monitoring/base.py +++ b/backend/gn_module_monitoring/monitoring/base.py @@ -54,7 +54,7 @@ def MonitoringModel(self, object_type): monitoring_definitions = MonitoringDefinitions() - +monitoring_g_definitions = MonitoringDefinitions() class MonitoringObjectBase: _object_type = None diff --git a/backend/gn_module_monitoring/monitoring/definitions.py b/backend/gn_module_monitoring/monitoring/definitions.py index fd6b0c49f..00cd791a2 100644 --- a/backend/gn_module_monitoring/monitoring/definitions.py +++ b/backend/gn_module_monitoring/monitoring/definitions.py @@ -8,7 +8,7 @@ ) from .objects import MonitoringModule, MonitoringSite -from .base import monitoring_definitions +from .base import monitoring_definitions, monitoring_g_definitions from .repositories import MonitoringObject from .geom import MonitoringObjectGeom @@ -20,12 +20,12 @@ """ MonitoringModels_dict = { - "module": TMonitoringModules, - "site": TMonitoringSites, - "visit": TMonitoringVisits, - "observation": TMonitoringObservations, - "observation_detail": TMonitoringObservationDetails, - "sites_group": TMonitoringSitesGroups, + 'module': TMonitoringModules, + 'site': TMonitoringSites, + 'visit': TMonitoringVisits, + 'observation': TMonitoringObservations, + 'observation_detail': TMonitoringObservationDetails, + 'sites_group': TMonitoringSitesGroups, } MonitoringObjects_dict = { @@ -38,3 +38,14 @@ } monitoring_definitions.set(MonitoringObjects_dict, MonitoringModels_dict) + +# #####################"" +MonitoringModelsG_dict = { + x: MonitoringModels_dict[x] for x in MonitoringModels_dict if x not in "module" +} + +MonitoringObjectsG_dict = { + x: MonitoringObjects_dict[x] for x in MonitoringObjects_dict if x not in "module" +} + +monitoring_g_definitions.set(MonitoringObjectsG_dict, MonitoringModelsG_dict) diff --git a/backend/gn_module_monitoring/monitoring/objects.py b/backend/gn_module_monitoring/monitoring/objects.py index edd51870e..4b49f4932 100644 --- a/backend/gn_module_monitoring/monitoring/objects.py +++ b/backend/gn_module_monitoring/monitoring/objects.py @@ -26,9 +26,12 @@ class MonitoringSite(MonitoringObjectGeom): """ def preprocess_data(self, data): - module_ids = [module.id_module for module in self._model.modules] - id_module = int(data["id_module"]) - if id_module not in module_ids: - module_ids.append(id_module) + type_site_ids = [type_site.id_nomenclature_type_site for type_site in self._model.types_site] + if len(data['types_site']) >0 : + for id_type_site in data['types_site']: + if int(id_type_site) not in type_site_ids: + type_site_ids.append(id_type_site) + #TODO: A enlever une fois qu'on aura enelever le champ "id_nomenclature_type_site" du model et de la bdd + data["id_nomenclature_type_site"]=data["types_site"][0] - data["modules"] = module_ids + data['types_site'] = type_site_ids diff --git a/backend/gn_module_monitoring/monitoring/schemas.py b/backend/gn_module_monitoring/monitoring/schemas.py index 1816d677e..fcbbc76d3 100644 --- a/backend/gn_module_monitoring/monitoring/schemas.py +++ b/backend/gn_module_monitoring/monitoring/schemas.py @@ -32,7 +32,7 @@ class Meta: exclude = ("geom_geojson",) load_instance = True - medias = MA.Nested(MediaSchema) + medias = MA.Nested(MediaSchema,many=True) pk = fields.Method("set_pk",dump_only=True) geometry = fields.Method("serialize_geojson", dump_only=True) diff --git a/backend/gn_module_monitoring/monitoring/serializer.py b/backend/gn_module_monitoring/monitoring/serializer.py index 75bb82758..c474846a0 100644 --- a/backend/gn_module_monitoring/monitoring/serializer.py +++ b/backend/gn_module_monitoring/monitoring/serializer.py @@ -4,7 +4,7 @@ import datetime import uuid from flask import current_app -from .base import MonitoringObjectBase, monitoring_definitions +from .base import MonitoringObjectBase, monitoring_definitions, monitoring_g_definitions from ..utils.utils import to_int from ..routes.data_utils import id_field_name_dict from geonature.utils.env import DB @@ -12,15 +12,24 @@ class MonitoringObjectSerializer(MonitoringObjectBase): + + def get_parent(self): + monitoring_def = monitoring_g_definitions if self._module_code == "generic" else monitoring_definitions parent_type = self.parent_type() if not parent_type: return if not self._parent: - self._parent = monitoring_definitions.monitoring_object_instance( - self._module_code, parent_type, self.id_parent() - ).get() + self._parent = ( + monitoring_def + .monitoring_object_instance( + self._module_code, + parent_type, + self.id_parent() + ) + .get() + ) return self._parent @@ -57,7 +66,8 @@ def unflatten_specific_properties(self, properties): properties["data"] = data def serialize_children(self, depth): - children_types = self.config_param("children_types") + monitoring_def = monitoring_g_definitions if self._module_code == "generic" else monitoring_definitions + children_types = self.config_param('children_types') if not children_types: return @@ -74,8 +84,9 @@ def serialize_children(self, depth): children_of_type = [] for child_model in getattr(self._model, relation_name): - child = monitoring_definitions.monitoring_object_instance( - self._module_code, children_type, model=child_model + child = ( + monitoring_def + .monitoring_object_instance(self._module_code, children_type, model=child_model) ) children_of_type.append(child.serialize(depth)) @@ -169,7 +180,10 @@ def populate(self, post_data): self.preprocess_data(properties) # ajout des données en base - if hasattr(self._model, "from_geofeature"): + if hasattr(self._model, 'from_geofeature'): + for key in list(post_data): + if key not in ("properties","geometry","type"): + post_data.pop(key) self._model.from_geofeature(post_data, True) else: self._model.from_dict(properties, True) diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py index d4ee778aa..1bc413785 100644 --- a/backend/gn_module_monitoring/routes/site.py +++ b/backend/gn_module_monitoring/routes/site.py @@ -3,9 +3,11 @@ from werkzeug.datastructures import MultiDict from gn_module_monitoring.blueprint import blueprint -from gn_module_monitoring.monitoring.models import BibTypeSite, TMonitoringSites +from gn_module_monitoring.config.repositories import get_config +from gn_module_monitoring.monitoring.models import BibTypeSite, TMonitoringSites, TNomenclatures from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema from gn_module_monitoring.utils.routes import ( + create_or_update_object_api_sites_sites_group, filter_params, geojson_query, get_limit_page, @@ -34,6 +36,30 @@ def get_types_site(): ) +@blueprint.route("/sites/types/label", methods=["GET"]) +def get_types_site_by_label(): + params = MultiDict(request.args) + limit, page = get_limit_page(params=params) + sort_label, sort_dir = get_sort( + params=params, default_sort="label_fr", default_direction="desc" + ) + joinquery = BibTypeSite.query.join(BibTypeSite.nomenclature).filter( + TNomenclatures.label_fr.ilike(f"%{params['label_fr']}%") + ) + if sort_dir == "asc": + joinquery = joinquery.order_by(TNomenclatures.label_fr.asc()) + + # See if there are not too much labels since they are used + # in select in the frontend side. And an infinite select is not + # implemented + return paginate( + query=joinquery, + schema=BibTypeSiteSchema, + limit=limit, + page=page, + ) + + @blueprint.route("/sites/types/", methods=["GET"]) def get_type_site_by_id(id_type_site): res = BibTypeSite.find_by_id(id_type_site) @@ -83,3 +109,16 @@ def get_all_site_geometries(): def get_module_sites(module_code: str): # TODO: load with site_categories.json API return jsonify({"module_code": module_code}) + + +@blueprint.route("/sites", methods=["POST"]) +def post_sites(): + module_code = "generic" + object_type = "site" + customConfig = dict() + post_data = dict(request.get_json()) + for keys in post_data["dataComplement"].keys(): + if "config" in post_data["dataComplement"][keys]: + customConfig.update(post_data["dataComplement"][keys]["config"]) + get_config(module_code, force=True, customSpecConfig=customConfig) + return create_or_update_object_api_sites_sites_group(module_code, object_type), 201 diff --git a/backend/gn_module_monitoring/routes/sites_groups.py b/backend/gn_module_monitoring/routes/sites_groups.py index 8ebb8d3fd..28888ef30 100644 --- a/backend/gn_module_monitoring/routes/sites_groups.py +++ b/backend/gn_module_monitoring/routes/sites_groups.py @@ -5,9 +5,14 @@ from werkzeug.datastructures import MultiDict from gn_module_monitoring.blueprint import blueprint +from gn_module_monitoring.config.repositories import get_config +from gn_module_monitoring.modules.repositories import get_module +from gn_module_monitoring.monitoring.definitions import monitoring_g_definitions from gn_module_monitoring.monitoring.models import TMonitoringSites, TMonitoringSitesGroups from gn_module_monitoring.monitoring.schemas import MonitoringSitesGroupsSchema +from gn_module_monitoring.utils.errors.errorHandler import InvalidUsage from gn_module_monitoring.utils.routes import ( + create_or_update_object_api_sites_sites_group, filter_params, geojson_query, get_limit_page, @@ -15,7 +20,7 @@ paginate, sort, ) -from gn_module_monitoring.utils.errors.errorHandler import InvalidUsage +from gn_module_monitoring.utils.utils import to_int @blueprint.route("/sites_groups", methods=["GET"]) @@ -66,18 +71,12 @@ def get_sites_group_geometries(): @blueprint.route("/sites_groups/", methods=["PATCH"]) def patch(_id): - item_schema = MonitoringSitesGroupsSchema() - item_json = request.get_json() - item = TMonitoringSitesGroups.find_by_id(_id) - fields = TMonitoringSitesGroups.attribute_names() - for field in item_json: - if field in fields: - setattr(item, field, item_json[field]) - item_schema.load(item_json) - db.session.add(item) - - db.session.commit() - return item_schema.dump(item), 201 + # ###############################"" + # FROM route/monitorings + module_code = "generic" + object_type = "sites_group" + get_config(module_code, force=True) + return create_or_update_object_api_sites_sites_group(module_code, object_type, _id), 201 @blueprint.route("/sites_groups/", methods=["DELETE"]) @@ -91,16 +90,16 @@ def delete(_id): @blueprint.route("/sites_groups", methods=["POST"]) def post(): - item_schema = MonitoringSitesGroupsSchema() - item_json = request.get_json() - item = item_schema.load(item_json) - db.session.add(item) - db.session.commit() - return item_schema.dump(item), 201 + module_code = "generic" + object_type = "sites_group" + get_config(module_code, force=True) + return create_or_update_object_api_sites_sites_group(module_code, object_type), 201 @blueprint.errorhandler(ValidationError) def handle_validation_error(error): return InvalidUsage( - "Fields cannot be validated, message : {}".format(error.messages), status_code=422, payload=error.data + "Fields cannot be validated, message : {}".format(error.messages), + status_code=422, + payload=error.data, ).to_dict() diff --git a/backend/gn_module_monitoring/utils/routes.py b/backend/gn_module_monitoring/utils/routes.py index 2871d86dd..cbd0e0b95 100644 --- a/backend/gn_module_monitoring/utils/routes.py +++ b/backend/gn_module_monitoring/utils/routes.py @@ -1,8 +1,10 @@ from typing import Tuple -from flask import Response +from flask import Response, request from flask.json import jsonify from geonature.utils.env import DB +from gn_module_monitoring.modules.repositories import get_module +from gn_module_monitoring.utils.utils import to_int from marshmallow import Schema from sqlalchemy import cast, func, text from sqlalchemy.dialects.postgresql import JSON @@ -11,6 +13,7 @@ from gn_module_monitoring.monitoring.queries import Query as MonitoringQuery from gn_module_monitoring.monitoring.schemas import paginate_schema +from gn_module_monitoring.monitoring.definitions import monitoring_g_definitions def get_limit_page(params: MultiDict) -> Tuple[int]: @@ -57,3 +60,44 @@ def geojson_query(subquery) -> bytes: if len(result) > 0: return result[0] return b"" + + +def create_or_update_object_api_sites_sites_group(module_code, object_type, id=None): + """ + route pour la création ou la modification d'un objet + si id est renseigné, c'est une création (PATCH) + sinon c'est une modification (POST) + + :param module_code: reference le module concerne + :param object_type: le type d'object (site, visit, obervation) + :param id : l'identifiant de l'object (de id_base_site pour site) + :type module_code: str + :type object_type: str + :type id: int + :return: renvoie l'object crée ou modifié + :rtype: dict + """ + depth = to_int(request.args.get("depth", 1)) + + # recupération des données post + post_data = dict(request.get_json()) + if module_code != "generic": + module = get_module("module_code", module_code) + else: + module = {"id_module": "generic"} + # TODO : A enlever une fois que le post_data contiendra geometry et type depuis le front + if object_type == "site": + post_data["geometry"] = {"type": "Point", "coordinates": [2.5, 50]} + post_data["type"] = "Feature" + # on rajoute id_module s'il n'est pas renseigné par défaut ?? + if "id_module" not in post_data["properties"]: + module["id_module"] = "generic" + post_data["properties"]["id_module"] = module["id_module"] + else: + post_data["properties"]["id_module"] = module.id_module + + return ( + monitoring_g_definitions.monitoring_object_instance(module_code, object_type, id) + .create_or_update(post_data) + .serialize(depth) + ) diff --git a/frontend/app/class/monitoring-site.ts b/frontend/app/class/monitoring-site.ts deleted file mode 100644 index d33650386..000000000 --- a/frontend/app/class/monitoring-site.ts +++ /dev/null @@ -1,13 +0,0 @@ -export enum columnNameSite { - base_site_name = "Nom", - last_visit = "Dernière visite", - nb_visits = "Nb. visites", - base_site_code = "Code", - altitude_max = "Alt.max", - altitude_min = "Alt.min", -} - -export const extendedDetailsSite = { - ...columnNameSite, - base_site_description: "Description", -}; diff --git a/frontend/app/class/monitoring-sites-group.ts b/frontend/app/class/monitoring-sites-group.ts deleted file mode 100644 index 96fab297d..000000000 --- a/frontend/app/class/monitoring-sites-group.ts +++ /dev/null @@ -1,12 +0,0 @@ -export enum columnNameSiteGroup { - sites_group_name = "Nom", - nb_sites = "Nb. sites", - nb_visits = "Nb. visites", - sites_group_code = "Code", -} - -export const extendedDetailsSiteGroup = { - ...columnNameSiteGroup, - comments: "Commentaires", - sites_group_description: "Description", -}; diff --git a/frontend/app/components/btn-select/btn-select.component.css b/frontend/app/components/btn-select/btn-select.component.css new file mode 100644 index 000000000..025878545 --- /dev/null +++ b/frontend/app/components/btn-select/btn-select.component.css @@ -0,0 +1,5 @@ +.example-chip-list { + width: 100%; + background-color: white; + border-radius: 5px; +} diff --git a/frontend/app/components/btn-select/btn-select.component.html b/frontend/app/components/btn-select/btn-select.component.html new file mode 100644 index 000000000..c852dfbd1 --- /dev/null +++ b/frontend/app/components/btn-select/btn-select.component.html @@ -0,0 +1,31 @@ + + {{ titleBtn }} + + + {{ optionSelected }} + + + + + + + + {{ option.name }} + + + + diff --git a/frontend/app/components/btn-select/btn-select.component.ts b/frontend/app/components/btn-select/btn-select.component.ts new file mode 100644 index 000000000..345a4ab6e --- /dev/null +++ b/frontend/app/components/btn-select/btn-select.component.ts @@ -0,0 +1,123 @@ +import { COMMA, ENTER } from '@angular/cdk/keycodes'; +import { + Component, + ElementRef, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, +} from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { Observable, iif, of } from 'rxjs'; +import { debounceTime, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'; + +import { JsonData } from '../../types/jsondata'; + +export interface EmptyObject { + name: string; +} + +@Component({ + selector: 'btn-select', + templateUrl: './btn-select.component.html', + styleUrls: ['./btn-select.component.css'], +}) +export class BtnSelectComponent implements OnInit { + selectable = true; + removable = true; + separatorKeysCodes: number[] = [ENTER, COMMA]; + myControl = new FormControl(); + @Input() placeholderText: string = 'Selectionnez vos options dans la liste'; + @Input() titleBtn: string = 'Choix des options'; + + filteredOptions: Observable; + listOptionChosen: string[] = []; + configObjAdded: JsonData = {}; + genericResponse: JsonData = {}; + + @Input() paramToFilt: string; + @Input() callBackFunction: ( + pageNumber: number, + limit: number, + valueToFilter: string + ) => Observable; + @ViewChild('optionInput') optionInput: ElementRef; + + @Output() public sendobject = new EventEmitter(); + + constructor() {} + + ngOnInit() { + this.filteredOptions = this.myControl.valueChanges.pipe( + startWith(''), + debounceTime(400), + distinctUntilChanged(), + switchMap((val: string) => { + return iif( + () => val == '', + of([{ name: val }]), + this.filterOnRequest(val, this.paramToFilt) + ); + }), + map((res) => (res.length > 0 ? res : [{ name: 'Pas de résultats' }])) + ); + } + + remove(option: string): void { + const index = this.listOptionChosen.indexOf(option); + + if (index >= 0) { + this.listOptionChosen.splice(index, 1); + } + + if (this.configObjAdded && this.configObjAdded[option] !== undefined) { + delete this.configObjAdded[option]; + } + this.sendobject.emit(this.configObjAdded); + } + + selected(event: MatAutocompleteSelectedEvent): void { + const shouldAddValue = this.checkBeforeAdding(event.option.viewValue); + shouldAddValue + ? this.listOptionChosen.push(event.option.viewValue) && this.addObject(event.option.value) + : null; + this.optionInput.nativeElement.value = ''; + this.myControl.setValue(null); + } + + filterOnRequest(val: string, keyToFilt: string): Observable { + return this.callBackFunction(1, 100, val).pipe( + // Ici on map pour créer une liste d'objet contenant la valeur entré + map((response) => + response.items.filter((option) => { + return option[keyToFilt].toLowerCase().includes(val.toLowerCase()); + }) + ), + // Ici on map pour uniformiser la "key" utilisé pour afficher les options (default Key : 'name') + map((response) => + response.filter((obj) => { + Object.assign(obj, { name: obj[keyToFilt] })[keyToFilt]; + delete obj[keyToFilt]; + return obj; + }) + ) + ); + } + + checkBeforeAdding(valToAdd: string) { + const noValidInput = [null, '', 'Pas de résultats']; + if (noValidInput.includes(valToAdd) || this.listOptionChosen.includes(valToAdd)) { + return false; + } else { + return true; + } + } + + addObject(obj: JsonData) { + const { name, ...configAndId } = obj; + this.configObjAdded[name] = configAndId; + this.sendobject.emit(this.configObjAdded); + } +} diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css index 6017b44ab..11176aee6 100644 --- a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css @@ -1,48 +1,47 @@ .cell-link { - cursor: pointer; + cursor: pointer; } :host::ng-deep .datatable-body-row.active .datatable-row-group { - background-color: rgb(117, 227, 118) !important; + background-color: rgb(117, 227, 118) !important; } .link:hover { - background-color: rgba(0, 0, 0, 0.2) !important; - transition: background-color 0.5; + background-color: rgba(0, 0, 0, 0.2) !important; + transition: background-color 0.5; } .link { - display: inline; - transition: background-color 0.5s; - border-radius: 5px; + display: inline; + transition: background-color 0.5s; + border-radius: 5px; } .header-filter-span > input { - width: 100%; + width: 100%; } .header-sort-span { - /* width: 100%; */ - cursor: pointer; - text-overflow: ellipsis; - overflow: hidden; - white-space:nowrap + /* width: 100%; */ + cursor: pointer; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; } - .header-sort-span:hover { - background-color: rgb(245, 245, 245); + background-color: rgb(245, 245, 245); } .icon-sort { - font-size: 1.2em; - float: right; + font-size: 1.2em; + float: right; } :host::ng-deep .sort-btn { - display: none !important; + display: none !important; } .custom-dt { - box-shadow: none !important; + box-shadow: none !important; } diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html index 17e0a03ed..7db5849a0 100644 --- a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html @@ -17,11 +17,8 @@ - - + - +
help - - {{ column.name }} + {{ column.name }}
{ beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ MonitoringDatatableComponent ] - }) - .compileComponents(); + declarations: [MonitoringDatatableComponent], + }).compileComponents(); })); beforeEach(() => { diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts index b152c85cd..73bb836d8 100644 --- a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts @@ -1,21 +1,23 @@ -import { DatatableComponent } from "@swimlane/ngx-datatable"; import { Component, - OnInit, + EventEmitter, Input, + OnInit, Output, - EventEmitter, - ViewChild, SimpleChanges, TemplateRef, -} from "@angular/core"; -import { Router, ActivatedRoute } from "@angular/router"; -import { Subject } from "rxjs"; -import { debounceTime } from "rxjs/operators"; -import { DataTableService } from "../../services/data-table.service"; -import { IColumn } from "../../interfaces/column"; -import { IPage } from "../../interfaces/page"; -import { ObjectService } from "../../services/object.service"; + ViewChild, +} from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { DatatableComponent } from '@swimlane/ngx-datatable'; +import { Subject } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +import { IColumn } from '../../interfaces/column'; +import { IobjObs, ObjDataType } from '../../interfaces/objObs'; +import { IPage } from '../../interfaces/page'; +import { DataTableService } from '../../services/data-table.service'; +import { ObjectService } from '../../services/object.service'; interface ItemObjectTable { id: number | null; @@ -26,9 +28,9 @@ interface ItemObjectTable { type ItemsObjectTable = { [key: string]: ItemObjectTable }; @Component({ - selector: "pnx-monitoring-datatable-g", - templateUrl: "./monitoring-datatable-g.component.html", - styleUrls: ["./monitoring-datatable-g.component.css"], + selector: 'pnx-monitoring-datatable-g', + templateUrl: './monitoring-datatable-g.component.html', + styleUrls: ['./monitoring-datatable-g.component.css'], }) export class MonitoringDatatableGComponent implements OnInit { @Input() rows; @@ -53,15 +55,15 @@ export class MonitoringDatatableGComponent implements OnInit { displayFilter: boolean = false; objectsStatus: ItemsObjectTable; - objectType: string = ""; + objectType: IobjObs; columns; row_save; selected = []; filters = {}; @ViewChild(DatatableComponent) table: DatatableComponent; - @ViewChild("actionsTemplate") actionsTemplate: TemplateRef; - @ViewChild("hdrTpl") hdrTpl: TemplateRef; + @ViewChild('actionsTemplate') actionsTemplate: TemplateRef; + @ViewChild('hdrTpl') hdrTpl: TemplateRef; constructor( private _dataTableService: DataTableService, @@ -108,7 +110,7 @@ export class MonitoringDatatableGComponent implements OnInit { // filter all const oldFilters = this.filters; this.filters = Object.keys(oldFilters).reduce(function (r, e) { - if (![undefined, "", null].includes(oldFilters[e])) r[e] = oldFilters[e]; + if (![undefined, '', null].includes(oldFilters[e])) r[e] = oldFilters[e]; return r; }, {}); this.onFilter.emit(this.filters); @@ -123,7 +125,7 @@ export class MonitoringDatatableGComponent implements OnInit { this.rowStatus.forEach((status) => { const bCond = status.id === id; - status["selected"] = bCond && !status["selected"]; + status['selected'] = bCond && !status['selected']; }); this.setSelected(); @@ -132,7 +134,6 @@ export class MonitoringDatatableGComponent implements OnInit { setSelected() { // this.table._internalRows permet d'avoir les ligne triées et d'avoir les bons index - if (!this.rowStatus) { return; } @@ -165,27 +166,21 @@ export class MonitoringDatatableGComponent implements OnInit { ngOnChanges(changes: SimpleChanges) { // IF prefered ngOnChanges compare to observable uncomment this: - if (changes["rows"] && this.rows && this.rows.length > 0) { - this.columns = this._dataTableService.colsTable( - this.colsname, - this.rows[0] - ); + if (changes['rows'] && this.rows && this.rows.length > 0) { + this.columns = this._dataTableService.colsTable(this.colsname, this.rows[0]); } - if (changes["colsname"]) { + if (changes['colsname']) { this.filters = {}; } - if (changes["obj"] && this.obj) { + if (changes['obj'] && this.obj) { this.objectsStatus, - (this.rowStatus = this._dataTableService.initObjectsStatus( - this.obj, - "sites_groups" - )); + (this.rowStatus = this._dataTableService.initObjectsStatus(this.obj, 'sites_groups')); } for (const propName of Object.keys(changes)) { switch (propName) { - case "rowStatus": + case 'rowStatus': this.setSelected(); break; } @@ -193,12 +188,13 @@ export class MonitoringDatatableGComponent implements OnInit { } navigateToAddChildren(_, rowId) { this.addEvent.emit(rowId); - this.router.navigate(["create"], { + this._objService.changeObjectType(this.objectType); + this.router.navigate(['create'], { relativeTo: this._Activatedroute, }); } navigateToDetail(row) { - row["id"] = row.pk; + row['id'] = row.pk; this.onDetailsRow.emit(row); } } diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.css b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.css index a5c26b9ca..d4ac1536b 100644 --- a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.css +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.css @@ -13,10 +13,9 @@ } .float-right { -margin-left: 5px; + margin-left: 5px; } - .float-left { margin-right: 10px; float: left; @@ -28,4 +27,20 @@ form:invalid { form.ng-invalid { border: 0px !important; -} \ No newline at end of file +} + +#form-scroll { + overflow-y: auto; + max-height: 70vh; +} + +.btn-child { + border: 0px solid #202020; + padding-top: 2px; + padding-bottom: auto; + -webkit-box-shadow: 0px -2px 2px rgba(50, 50, 50, 0.3); + -moz-box-shadow: 0px -2px 2px rgba(50, 50, 50, 0.3); + box-shadow: 0px -2px 2px rgba(50, 50, 50, 0.3); + overflow: hidden; + height: fit-content; +} diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html index 389e9284b..7f4f5f46e 100644 --- a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html @@ -1,6 +1,6 @@

Attention

-

+

Vous êtes sur le point de supprimer le groupe de site Description du groupe de site @@ -14,22 +14,16 @@

Attention

> Confirmer la suppression - +
-
- - - +
+ + - - +
- - - -
+
+ +
+ +
- Valider - - - - +
diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.spec.ts b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.spec.ts index 1cfc81be0..be4c9c70d 100644 --- a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.spec.ts +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.spec.ts @@ -1,4 +1,4 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { MonitoringFormComponent } from './monitoring-form.component'; @@ -8,9 +8,8 @@ describe('MonitoringFormComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ MonitoringFormComponent ] - }) - .compileComponents(); + declarations: [MonitoringFormComponent], + }).compileComponents(); })); beforeEach(() => { diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts index c993c5026..683fbe72f 100644 --- a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts @@ -1,25 +1,22 @@ -import { - Component, - OnInit, - Input, - Output, - EventEmitter, - SimpleChanges, -} from "@angular/core"; -import { FormGroup, FormBuilder } from "@angular/forms"; -import { MonitoringObject } from "../../class/monitoring-object"; -import { ConfigService } from "../../services/config.service"; -import { CommonService } from "@geonature_common/service/common.service"; -import { DynamicFormService } from "@geonature_common/form/dynamic-form-generator/dynamic-form.service"; -import { ActivatedRoute } from "@angular/router"; -import { EditObjectService } from "../../services/edit-object.service"; -import { Router } from "@angular/router"; -import { IDataForm } from "../../interfaces/form"; -import { ApiGeomService } from "../../services/api-geom.service"; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { tap, mergeMap } from 'rxjs/operators'; +import { ActivatedRoute } from '@angular/router'; +import { Router } from '@angular/router'; +import { DynamicFormService } from '@geonature_common/form/dynamic-form-generator/dynamic-form.service'; +import { CommonService } from '@geonature_common/service/common.service'; + +import { MonitoringObject } from '../../class/monitoring-object'; +import { IDataForm } from '../../interfaces/form'; +import { ApiGeomService } from '../../services/api-geom.service'; +import { ConfigJsonService } from '../../services/config-json.service'; +import { FormService } from '../../services/form.service'; +import { ObjectService } from '../../services/object.service'; + @Component({ - selector: "pnx-monitoring-form-g", - templateUrl: "./monitoring-form.component-g.html", - styleUrls: ["./monitoring-form.component-g.css"], + selector: 'pnx-monitoring-form-g', + templateUrl: './monitoring-form.component-g.html', + styleUrls: ['./monitoring-form.component-g.css'], }) export class MonitoringFormComponentG implements OnInit { @Input() currentUser; @@ -37,7 +34,7 @@ export class MonitoringFormComponentG implements OnInit { @Input() sites: {}; dataForm: IDataForm; - searchSite = ""; + searchSite = ''; obj: any; objFormsDefinition; @@ -57,108 +54,103 @@ export class MonitoringFormComponentG implements OnInit { constructor( private _formBuilder: FormBuilder, private _route: ActivatedRoute, - private _configService: ConfigService, + private _configService: ConfigJsonService, private _commonService: CommonService, private _dynformService: DynamicFormService, - private _editService: EditObjectService, + private _formService: FormService, private _apiGeomService: ApiGeomService, - private _router: Router + private _router: Router, + private _objService: ObjectService ) {} ngOnInit() { - this._editService.currentData.subscribe((dataToEdit) => { - this.obj = dataToEdit; - this.obj.bIsInitialized = true; - this._configService - .init(this.obj.moduleCode) - .pipe() - .subscribe(() => { - // return this._route.queryParamMap; - // }) - // .subscribe((queryParams) => { - this.queryParams = this._route.snapshot.queryParams || {}; - this.bChainInput = - this._configService.frontendParams()["bChainInput"]; - - const schema = this._configService.schema( + // TODO: Avoid two subscribes one inside other (code test above doesn't work. When add type site the observable currentdata is not recall) + this._formService.currentData + .pipe( + tap((data) => { + this.obj = data; + this.obj.bIsInitialized = true; + this._apiGeomService.init(this.obj.endPoint, this.obj.objSelected); + }), + mergeMap((data: any) => this._configService.init(data.moduleCode)) + ) + .subscribe(() => { + + this.queryParams = this._route.snapshot.queryParams || {}; + this.bChainInput = this._configService.frontendParams()['bChainInput']; + + const schema = this._configService.schema( + this.obj.moduleCode, + this.obj.objectType + ); + + this.obj[this.obj.moduleCode] = schema; + + this.obj.specific == undefined ? (this.obj.specific = {}) : null; + if (Object.keys(this.obj.specific).length !== 0) { + Object.assign(schema, this.obj.specific); + } + + + + // meta pour les parametres dynamiques + // ici pour avoir acces aux nomenclatures + this.meta = { + // nomenclatures: this._dataUtilsService.getDataUtil('nomenclature'), + // dataset: this._dataUtilsService.getDataUtil('dataset'), + // id_role: this.currentUser.id_role, + bChainInput: this.bChainInput, + parents: this.obj.parents, + }; + + this.objFormsDefinition = this._dynformService + .formDefinitionsdictToArray(schema, this.meta) + .filter((formDef) => formDef.type_widget) + .sort((a, b) => { + // medias à la fin + return a.attribut_name === 'medias' + ? +1 + : b.attribut_name === 'medias' + ? -1 + : 0; + }); + + // display_form pour customiser l'ordre dans le formulaire + // les éléments de display form sont placé en haut dans l'ordre du tableau + // tous les éléments non cachés restent affichés + + let displayProperties = [ + ...(this._configService.configModuleObjectParam( this.obj.moduleCode, - this.obj.objectType - ); - this.obj[this.obj.moduleCode] = schema; - // const schema = this.obj.schema(); - - // init objFormsDefinition - - // meta pour les parametres dynamiques - // ici pour avoir acces aux nomenclatures - this.meta = { - // nomenclatures: this._dataUtilsService.getDataUtil('nomenclature'), - // dataset: this._dataUtilsService.getDataUtil('dataset'), - // id_role: this.currentUser.id_role, - bChainInput: this.bChainInput, - parents: this.obj.parents, - }; - - this.objFormsDefinition = this._dynformService - .formDefinitionsdictToArray(schema, this.meta) - .filter((formDef) => formDef.type_widget) - .sort((a, b) => { - // medias à la fin - return a.attribut_name === "medias" - ? +1 - : b.attribut_name === "medias" - ? -1 - : 0; - }); - - // display_form pour customiser l'ordre dans le formulaire - // les éléments de display form sont placé en haut dans l'ordre du tableau - // tous les éléments non cachés restent affichés - - let displayProperties = [ - ...(this._configService.configModuleObjectParam( - this.obj.moduleCode, - this.obj.objectType, - "display_properties" - ) || []), - ]; - if (displayProperties && displayProperties.length) { - displayProperties.reverse(); - this.objFormsDefinition.sort((a, b) => { - let indexA = displayProperties.findIndex( - (e) => e == a.attribut_name - ); - let indexB = displayProperties.findIndex( - (e) => e == b.attribut_name - ); - return indexB - indexA; - }); - } - - // champs patch pour simuler un changement de valeur et déclencher le recalcul des propriété - // par exemple quand bChainInput change - this.objForm.addControl("patch_update", this._formBuilder.control(0)); - - // this._configService.configModuleObject(this.obj.moduleCode, this.obj.objectType); - // set geometry - // if (this.obj.config["geometry_type"]) { - // this.objForm.addControl( - // "geometry", - // this._formBuilder.control("", Validators.required) - // ); - // } - - // pour donner la valeur de idParent - - this.initForm(); - }); - }); + this.obj.objectType, + 'display_properties' + ) || []), + ]; + if (displayProperties && displayProperties.length) { + displayProperties.reverse(); + this.objFormsDefinition.sort((a, b) => { + let indexA = displayProperties.findIndex( + (e) => e == a.attribut_name + ); + let indexB = displayProperties.findIndex( + (e) => e == b.attribut_name + ); + return indexB - indexA; + }); + } + + // champs patch pour simuler un changement de valeur et déclencher le recalcul des propriété + // par exemple quand bChainInput change + this.objForm.addControl('patch_update', this._formBuilder.control(0)); + + this.initForm(); + }); } /** pour réutiliser des paramètres déjà saisis */ keepDefinitions() { return this.objFormsDefinition.filter((def) => - this.obj.configParam("keep").includes(def.attribut_name) + this.obj.configParam('keep').includes(def.attribut_name) ); } @@ -182,7 +174,7 @@ export class MonitoringFormComponentG implements OnInit { this.setQueryParams(); // pour donner la valeur de l'objet au formulaire - this._editService.formValues(this.obj).subscribe((formValue) => { + this._formService.formValues(this.obj).subscribe((formValue) => { this.objForm.patchValue(formValue); this.setDefaultFormValue(); this.dataForm = formValue; @@ -191,16 +183,14 @@ export class MonitoringFormComponentG implements OnInit { } keepNames() { - return this.obj.configParam("keep") || []; + return this.obj.configParam('keep') || []; } resetObjForm() { // quand on enchaine les relevés - const chainShow = this.obj.configParam("chain_show"); + const chainShow = this.obj.configParam('chain_show'); if (chainShow) { - this.chainShow.push( - chainShow.map((key) => this.obj.resolvedProperties[key]) - ); + this.chainShow.push(chainShow.map((key) => this.obj.resolvedProperties[key])); this.chainShow.push(this.obj.resolvedProperties); } @@ -219,7 +209,7 @@ export class MonitoringFormComponentG implements OnInit { ); this.obj.init({}); - this.obj.properties[this.obj.configParam("id_field_Name")] = null; + this.obj.properties[this.obj.configParam('id_field_Name')] = null; // pq get ????? // this.obj.get(0).subscribe(() => { @@ -231,7 +221,7 @@ export class MonitoringFormComponentG implements OnInit { this.objChanged.emit(this.obj); this.objForm.patchValue({ geometry: null }); this.initForm(); - // }); + // }; } /** Pour donner des valeurs par defaut si la valeur n'est pas définie @@ -245,7 +235,7 @@ export class MonitoringFormComponentG implements OnInit { const defaultValue = { // id_digitiser: value["id_digitiser"] || this.currentUser.id_role, // id_inventor: value["id_inventor"] || this.currentUser.id_role, - first_use_date: value["first_use_date"] || { + first_use_date: value['first_use_date'] || { year: date.getUTCFullYear(), month: date.getUTCMonth() + 1, day: date.getUTCDate(), @@ -272,11 +262,12 @@ export class MonitoringFormComponentG implements OnInit { navigateToDetail(id, objectType, queryParams) { // patch bug navigation this._router.navigate( - ["monitorings", objectType, id].filter((s) => !!s), + ['monitorings', objectType, id].filter((s) => !!s), { queryParams, } ); + this.objChanged.emit(this.obj); this.bEditChange.emit(false); } @@ -285,7 +276,7 @@ export class MonitoringFormComponentG implements OnInit { */ navigateToParent() { this.bEditChange.emit(false); // patch bug navigation - this._router.navigateByUrl("/monitorings/sites_group"); + this._router.navigateByUrl('/monitorings/sites_group'); // this.obj.navigateToParent(); } @@ -298,22 +289,19 @@ export class MonitoringFormComponentG implements OnInit { /** TODO améliorer site etc.. */ onSubmit() { const { patch_update, ...sendValue } = this.dataForm; + const objToUpdateOrCreate = this._formService.postData(sendValue, this.obj); + console.log(objToUpdateOrCreate); const action = this.obj.id - ? // ? this.obj.patch(this.objForm.value) - // : this.obj.post(this.objForm.value); - this._apiGeomService.patch(this.obj.id, sendValue) - : this._apiGeomService.create(sendValue); - const actionLabel = this.obj.id ? "Modification" : "Création"; + ? this._apiGeomService.patch(this.obj.id, objToUpdateOrCreate) + : this._apiGeomService.create(objToUpdateOrCreate); + const actionLabel = this.obj.id ? 'Modification' : 'Création'; action.subscribe((objData) => { - this._commonService.regularToaster( - "success", - this.msgToaster(actionLabel) - ); + this._commonService.regularToaster('success', this.msgToaster(actionLabel)); this.bSaveSpinner = this.bSaveAndAddChildrenSpinner = false; - this.objChanged.emit(this.obj); + // this.objChanged.emit(this.obj); /** si c'est un module : reset de la config */ - if (this.obj.objectType === "module") { + if (this.obj.objectType === 'module') { this._configService.loadConfig(this.obj.moduleCode).subscribe(); } @@ -326,16 +314,12 @@ export class MonitoringFormComponentG implements OnInit { this._configService.configModuleObjectParam( this.obj.moduleCode, this.obj.objectType, - "redirect_to_parent" + 'redirect_to_parent' ) ) { this.navigateToParent(); } else { - this.navigateToDetail( - this.obj.id, - this.obj.objectType, - this.queryParams - ); + this.navigateToDetail(this.obj.id, this.obj.objectType, this.queryParams); } } }); @@ -351,7 +335,7 @@ export class MonitoringFormComponentG implements OnInit { onDelete() { this.bDeleteSpinner = true; - this._commonService.regularToaster("info", this.msgToaster("Suppression")); + this._commonService.regularToaster('info', this.msgToaster('Suppression')); // : this.obj.post(this.objForm.value); this._apiGeomService.delete(this.obj.id).subscribe((del) => { this.bDeleteSpinner = this.bDeleteModal = false; @@ -366,10 +350,7 @@ export class MonitoringFormComponentG implements OnInit { // let {id_module,medias, ...rest} = this.objForm.value; // this.dataForm = rest this.dataForm = this.objForm.value; - const change = this._configService.change( - this.obj.moduleCode, - this.obj.objectType - ); + const change = this._configService.change(this.obj.moduleCode, this.obj.objectType); if (!change) { return; } @@ -389,8 +370,24 @@ export class MonitoringFormComponentG implements OnInit { for (const formDef of this.objFormsDefinition) { formDef.meta.bChainInput = this.bChainInput; } - this._configService.setFrontendParams("bChainInput", this.bChainInput); + this._configService.setFrontendParams('bChainInput', this.bChainInput); // patch pour recalculers this.procesPatchUpdateForm(); } + + getConfigFromBtnSelect(event) { + // this.obj.specific == undefined ? (this.obj.specific = {}) : null; + // TODO: Ajout de tous les id_parents ["id_sites_groups" etc ] dans l'objet obj.dataComplement + this.obj.specific = {}; + this.obj.dataComplement = {}; + for (const key in event) { + if (event[key].config != undefined) { + if (Object.keys(event[key].config).length !== 0) { + Object.assign(this.obj.specific, event[key].config.specific); + } + } + } + Object.assign(this.obj.dataComplement, event); + this._formService.dataToCreate(this.obj); + } } diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html index ebab4aca9..e95701d15 100644 --- a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html @@ -13,7 +13,7 @@ >Propriétés
@@ -39,27 +39,20 @@ attr.aria-labelledby="nav-properties-tab" > - + - +
- {{ fieldName.value }} - + {{ fields[fieldName] }} + help {{ selectedObj[fieldName.key] }}{{ selectedObj[fieldName] }}
@@ -68,9 +61,9 @@ id="medias-tab" role="tabpanel" attr.aria-labelledby="nav-medias-tab" - *ngIf="obj.properties['medias'] && obj.properties['medias'].length" + *ngIf="objectType.properties['medias'] && objectType.properties['medias'].length" > - +
{{ media.title_fr }} @@ -83,7 +76,7 @@

{{media.description_fr}}

--> -
diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts index d47a3ef60..0c4cf5456 100644 --- a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts @@ -1,16 +1,10 @@ -import { - Component, - OnInit, - Input, - Output, - EventEmitter, - SimpleChanges, -} from "@angular/core"; +import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core"; import { FormControl } from "@angular/forms"; -import { extendedDetailsSiteGroup } from "../../class/monitoring-sites-group"; import { ISitesGroup } from "../../interfaces/geom"; -import { EditObjectService } from "../../services/edit-object.service"; +import { IobjObs, ObjDataType } from "../../interfaces/objObs"; +import { FormService } from "../../services/form.service"; import { ObjectService } from "../../services/object.service"; +import { JsonData } from "../../types/jsondata"; @Component({ selector: "pnx-monitoring-properties-g", @@ -18,32 +12,44 @@ import { ObjectService } from "../../services/object.service"; styleUrls: ["./monitoring-properties-g.component.css"], }) export class MonitoringPropertiesGComponent implements OnInit { - @Input() selectedObj: ISitesGroup; + // selectedObj: ISitesGroup; + @Input() selectedObj: ObjDataType; @Input() bEdit: boolean; @Output() bEditChange = new EventEmitter(); - @Input() objectType: string; + @Input() objectType: IobjObs; - infosColsSiteGroups: typeof extendedDetailsSiteGroup = - extendedDetailsSiteGroup; color: string = "white"; dataDetails: ISitesGroup; - + fields: JsonData; + fieldDefinitions: JsonData; + fieldsNames: string[]; + endPoint:string; datasetForm = new FormControl(); constructor( - private _editService: EditObjectService, - private _objService: ObjectService + private _formService: FormService, + private _objService: ObjectService, ) {} ngOnInit() { this._objService.currentObjectTypeParent.subscribe((newObjType) => { this.objectType = newObjType; + this.fieldsNames = newObjType.template.fieldNames; + this.fields = newObjType.template.fieldLabels; + this.fieldDefinitions = newObjType.template.fieldDefinitions; + this.objectType.properties = this.selectedObj; + this.endPoint = newObjType.endPoint; }); } onEditClick() { this.bEditChange.emit(true); this.selectedObj["id"] = this.selectedObj[this.selectedObj.pk]; - this._editService.changeDataSub(this.selectedObj); + this._formService.changeDataSub( + this.selectedObj, + this.objectType.objectType, + this.objectType.endPoint, + this.objectType + ); } } diff --git a/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.css b/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.html b/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.html new file mode 100644 index 000000000..cc95c530c --- /dev/null +++ b/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.html @@ -0,0 +1,13 @@ + +
+ +
diff --git a/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.ts b/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.ts new file mode 100644 index 000000000..32514d56f --- /dev/null +++ b/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.ts @@ -0,0 +1,82 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { FormService } from '../../services/form.service'; +import { FormGroup, FormBuilder } from '@angular/forms'; +import { ISite, ISiteType } from '../../interfaces/geom'; +import { SitesService } from '../../services/api-geom.service'; +import { Observable } from 'rxjs'; +import { IobjObs, ObjDataType } from '../../interfaces/objObs'; +import { MonitoringFormComponentG } from '../monitoring-form-g/monitoring-form.component-g'; +import { ObjectService } from '../../services/object.service'; +import { JsonData } from '../../types/jsondata'; +import { endPoints } from '../../enum/endpoints'; +import { IPaginated } from '../../interfaces/page'; + +@Component({ + selector: 'monitoring-sites-create', + templateUrl: './monitoring-sites-create.component.html', + styleUrls: ['./monitoring-sites-create.component.css'], +}) +export class MonitoringSitesCreateComponent implements OnInit { + site: ISite; + form: FormGroup; + paramToFilt: string = 'label'; + funcToFilt: Function; + titleBtn: string = 'Choix des types de sites'; + placeholderText: string = 'Sélectionnez les types de site'; + id_sites_group: number; + types_site: string[]; + @ViewChild('subscritionObjConfig') + monitoringFormComponentG: MonitoringFormComponentG; + objToCreate: IobjObs; + + constructor( + private _formService: FormService, + private _formBuilder: FormBuilder, + private siteService: SitesService, + private _objService: ObjectService + ) {} + + ngOnInit() { + this._objService.currentObjSelected.subscribe((objParent) => { + this.id_sites_group = objParent.id_sites_group; + this._formService.dataToCreate({ + module: 'generic', + objectType: 'site', + id_sites_group: this.id_sites_group, + id_relationship: ['id_sites_group', 'types_site'], + endPoint: endPoints.sites, + objSelected: {}, + }); + this.form = this._formBuilder.group({}); + this.funcToFilt = this.partialfuncToFilt.bind(this); + }); + } + + partialfuncToFilt( + pageNumber: number, + limit: number, + valueToFilter: string + ): Observable> { + return this.siteService.getTypeSites(pageNumber, limit, { + label_fr: valueToFilter, + sort_dir: 'desc', + }); + } + + onSendConfig(config: JsonData): void { + config = this.addTypeSiteListIds(config); + this.monitoringFormComponentG.getConfigFromBtnSelect(config); + } + + addTypeSiteListIds(config: JsonData): JsonData { + if (config && config.length != 0) { + config.types_site = []; + for (const key in config) { + if ('id_nomenclature_type_site' in config[key]) { + config.types_site.push(config[key]['id_nomenclature_type_site']); + } + } + } + return config; + } +} diff --git a/frontend/app/components/monitoring-sites/monitoring-sites.component.html b/frontend/app/components/monitoring-sites/monitoring-sites.component.html index 3294f13b8..0f495dfae 100644 --- a/frontend/app/components/monitoring-sites/monitoring-sites.component.html +++ b/frontend/app/components/monitoring-sites/monitoring-sites.component.html @@ -1,5 +1,4 @@ ; constructor( private _sitesGroupService: SitesGroupService, @@ -53,7 +53,7 @@ export class MonitoringSitesComponent ngOnInit() { this.objForm = this._formBuilder.group({}); - this._objService.changeObjectType(this._siteService.addObjectType()); + this._objService.changeObjectType(this._siteService.objectObs); this.initSite(); } @@ -74,10 +74,10 @@ export class MonitoringSitesComponent id_sites_group: id, }), }) - ) - ) + )) .subscribe( - (data: { sitesGroup: ISitesGroup; sites: IPaginated }) => { + (data: { sitesGroup: ISitesGroup; sites: IPaginated}) => { + this._objService.changeSelectedObj(data.sitesGroup, true); this.sitesGroup = data.sitesGroup; this.sites = data.sites.items; this.page = { @@ -90,6 +90,7 @@ export class MonitoringSitesComponent () => {} ); this.baseFilters = { id_sites_group: this.sitesGroup.id_sites_group }; + this.colsname = this._siteService.objectObs.dataTable.colNameObj; } ); } @@ -126,7 +127,7 @@ export class MonitoringSitesComponent } seeDetails($event) { - this._objService.changeObjectTypeParent(this._siteService.editObjectType()); + this._objService.changeObjectTypeParent(this._siteService.objectObs, true); this.router.navigate([`sites/${$event.id_base_site}`], { relativeTo: this._Activatedroute, }); diff --git a/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts index 4138a321b..07c4e379b 100644 --- a/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts +++ b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts @@ -1,7 +1,8 @@ import { Component, OnInit } from "@angular/core"; -import { EditObjectService } from "../../services/edit-object.service"; +import { FormService } from "../../services/form.service"; import { FormGroup, FormBuilder } from "@angular/forms"; import { ISitesGroup } from "../../interfaces/geom"; +import { endPoints } from "../../enum/endpoints"; @Component({ selector: "monitoring-sitesgroups-create", @@ -12,12 +13,17 @@ export class MonitoringSitesGroupsCreateComponent implements OnInit { siteGroup: ISitesGroup; form: FormGroup; constructor( - private _editService: EditObjectService, + private _formService: FormService, private _formBuilder: FormBuilder ) {} ngOnInit() { - this._editService.changeDataSub({}); + this._formService.dataToCreate({ + module: "generic", + objectType: "sites_group", + endPoint:endPoints.sites_groups, + objSelected: {} + }); this.form = this._formBuilder.group({}); } } diff --git a/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts b/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts index 88473a04f..d7dc6a662 100644 --- a/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts +++ b/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts @@ -1,15 +1,15 @@ import { Component, OnInit, Input } from "@angular/core"; import { SitesGroupService } from "../../services/api-geom.service"; -import { columnNameSiteGroup } from "../../class/monitoring-sites-group"; import { IPaginated, IPage } from "../../interfaces/page"; import { Router, ActivatedRoute } from "@angular/router"; -import { columnNameSite } from "../../class/monitoring-site"; import { ISite, ISitesGroup } from "../../interfaces/geom"; import { GeoJSONService } from "../../services/geojson.service"; import { MonitoringGeomComponent } from "../../class/monitoring-geom-component"; import { setPopup } from "../../functions/popup"; import { ObjectService } from "../../services/object.service"; import { FormGroup, FormBuilder } from "@angular/forms"; +import { IobjObs } from "../../interfaces/objObs"; +import { ConfigJsonService } from "../../services/config-json.service"; const LIMIT = 10; @@ -25,15 +25,12 @@ export class MonitoringSitesGroupsComponent @Input() page: IPage; @Input() sitesGroups: ISitesGroup[]; @Input() sitesChild: ISite[]; - @Input() columnNameSiteGroup: typeof columnNameSiteGroup = - columnNameSiteGroup; - @Input() columnNameSite: typeof columnNameSite = columnNameSite; @Input() sitesGroupsSelected: ISitesGroup; // @Input() rows; - @Input() colsname; @Input() obj; - objectType: string; + colsname: {}; + objectType: IobjObs; objForm: FormGroup; objInitForm: Object = {}; // siteGroupEmpty={ @@ -50,6 +47,7 @@ export class MonitoringSitesGroupsComponent private router: Router, private _objService: ObjectService, private _formBuilder: FormBuilder, + private _configJsonService: ConfigJsonService, private _Activatedroute: ActivatedRoute // private _routingService: RoutingService ) { super(); @@ -62,11 +60,12 @@ export class MonitoringSitesGroupsComponent initSiteGroup() { this._objService.changeObjectTypeParent( - this._sites_group_service.editObjectType() + this._sites_group_service.objectObs,true ); this._objService.changeObjectType( - this._sites_group_service.addObjectType() + this._sites_group_service.objectObs,true ); + this.getSitesGroups(1); this.geojsonService.getSitesGroupsGeometries( this.onEachFeatureSiteGroups() @@ -92,6 +91,7 @@ export class MonitoringSitesGroupsComponent } getSitesGroups(page = 1, params = {}) { + this._sites_group_service .get(page, LIMIT, params) .subscribe((data: IPaginated) => { @@ -101,7 +101,7 @@ export class MonitoringSitesGroupsComponent page: data.page - 1, }; this.sitesGroups = data.items; - this.colsname = this.columnNameSiteGroup; + this.colsname = this._sites_group_service.objectObs.dataTable.colNameObj; // IF prefered observable compare to ngOnChanges uncomment this: // this._dataTableService.changeColsTable(this.colsname,this.sitesGroups[0]) }); @@ -110,7 +110,7 @@ export class MonitoringSitesGroupsComponent seeDetails($event) { // TODO: routerLink this._objService.changeObjectTypeParent( - this._sites_group_service.editObjectType() + this._sites_group_service.objectObs,true ); this.router.navigate([$event.id_sites_group], { relativeTo: this._Activatedroute, diff --git a/frontend/app/enum/endpoints.ts b/frontend/app/enum/endpoints.ts new file mode 100644 index 000000000..fc7f634af --- /dev/null +++ b/frontend/app/enum/endpoints.ts @@ -0,0 +1,5 @@ + +export enum endPoints { + sites_groups = "sites_groups", + sites = "sites", + } \ No newline at end of file diff --git a/frontend/app/gnModule.module.ts b/frontend/app/gnModule.module.ts index 69016aefd..bb57b9cbe 100644 --- a/frontend/app/gnModule.module.ts +++ b/frontend/app/gnModule.module.ts @@ -8,11 +8,12 @@ import { HttpClient } from '@angular/common/http'; import { HttpClientXsrfModule } from '@angular/common/http'; // Service -import { DataMonitoringObjectService } from './services/data-monitoring-object.service'; -import { DataUtilsService } from './services/data-utils.service'; -import { CacheService } from './services/cache.service'; -import { MonitoringObjectService } from './services/monitoring-object.service'; -import { ConfigService } from './services/config.service'; +import { DataMonitoringObjectService } from "./services/data-monitoring-object.service"; +import { DataUtilsService } from "./services/data-utils.service"; +import { CacheService } from "./services/cache.service"; +import { MonitoringObjectService } from "./services/monitoring-object.service"; +import { ConfigService } from "./services/config.service"; +import { ConfigJsonService } from "./services/config-json.service"; // Component import { BreadcrumbsComponent } from "./components/breadcrumbs/breadcrumbs.component"; @@ -32,6 +33,7 @@ import { MatFormFieldModule } from "@angular/material/form-field"; import { MatAutocompleteModule } from "@angular/material/autocomplete"; import { MatSelectModule } from "@angular/material/select"; import { MatInputModule } from "@angular/material/input"; +import {MatChipsModule} from '@angular/material/chips'; import { MonitoringSitesGroupsComponent } from "./components/monitoring-sitesgroups/monitoring-sitesgroups.component"; import { DataTableService } from "./services/data-table.service"; import { MonitoringPropertiesGComponent } from "./components/monitoring-properties-g/monitoring-properties-g.component"; @@ -39,7 +41,7 @@ import { GeoJSONService } from "./services/geojson.service"; import { MonitoringSitesComponent } from "./components/monitoring-sites/monitoring-sites.component"; import { MonitoringMapListComponent } from "./components/monitoring-map-list/monitoring-map-list.component"; import { MonitoringFormComponentG } from "./components/monitoring-form-g/monitoring-form.component-g"; -import { EditObjectService } from "./services/edit-object.service"; +import { FormService } from "./services/form.service"; import { ObjectService } from "./services/object.service"; import { SitesGroupService, @@ -47,6 +49,8 @@ import { ApiGeomService, } from "./services/api-geom.service"; import { MonitoringSitesGroupsCreateComponent } from "./components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component"; +import { MonitoringSitesCreateComponent } from "./components/monitoring-sites-create/monitoring-sites-create.component"; +import { BtnSelectComponent } from "./components/btn-select/btn-select.component"; // my module routing const routes: Routes = [ @@ -79,12 +83,15 @@ const routes: Routes = [ { path: "create", component: MonitoringSitesGroupsCreateComponent }, { path: ":id", - // Add new component here children: [ { path: "", component: MonitoringSitesComponent, }, + { + path: "create", + component: MonitoringSitesCreateComponent, + }, ], }, ], @@ -110,6 +117,8 @@ const routes: Routes = [ MonitoringPropertiesGComponent, MonitoringFormComponentG, MonitoringSitesGroupsCreateComponent, + MonitoringSitesCreateComponent, + BtnSelectComponent ], imports: [ GN2CommonModule, @@ -122,6 +131,7 @@ const routes: Routes = [ MatAutocompleteModule, MatSelectModule, MatInputModule, + MatChipsModule, HttpClientXsrfModule.withOptions({ headerName: 'token', }), @@ -132,12 +142,13 @@ const routes: Routes = [ DataMonitoringObjectService, DataUtilsService, ConfigService, + ConfigJsonService, MonitoringObjectService, DataTableService, SitesGroupService, SitesService, GeoJSONService, - EditObjectService, + FormService, ObjectService, ApiGeomService, ], diff --git a/frontend/app/interfaces/geom.ts b/frontend/app/interfaces/geom.ts index 9d43b5cdf..51ac5de25 100644 --- a/frontend/app/interfaces/geom.ts +++ b/frontend/app/interfaces/geom.ts @@ -1,8 +1,8 @@ -import { GeoJSON } from "geojson"; -import { Observable } from "rxjs"; -import { JsonData } from "../types/jsondata"; -import { Resp } from "../types/response"; -import { IPaginated } from "./page"; +import { GeoJSON } from 'geojson'; +import { Observable } from 'rxjs'; +import { JsonData } from '../types/jsondata'; +import { Resp } from '../types/response'; +import { IPaginated } from './page'; export interface IGeomObject { data: JsonData; @@ -22,6 +22,7 @@ export interface ISitesGroup extends IGeomObject { } export interface ISite extends IGeomObject { + pk: number; altitude_max: number; altitude_min: number; base_site_code: string; @@ -37,14 +38,20 @@ export interface ISite extends IGeomObject { uuid_base_site: string; } +interface IGeomObjectProperties { + properties: IGeomObject; +} + export interface IGeomService { - get( - limit: number, - page: number, - params: JsonData - ): Observable>; + get(limit: number, page: number, params: JsonData): Observable>; get_geometries(params: JsonData): Observable; - create(postdata: IGeomObject): Observable; - patch(id: number, updatedData: IGeomObject): Observable; - // delete(obj: IGeomObject) + create(postdata: IGeomObjectProperties): Observable; + patch(id: number, updatedData: IGeomObjectProperties): Observable; + delete(id: number); +} + +export interface ISiteType { + config: JsonData; + id_nomenclature_type_site: number; + label: string; } diff --git a/frontend/app/interfaces/objObs.ts b/frontend/app/interfaces/objObs.ts new file mode 100644 index 000000000..927a65127 --- /dev/null +++ b/frontend/app/interfaces/objObs.ts @@ -0,0 +1,24 @@ +import { endPoints } from "../enum/endpoints"; +import { JsonData } from "../types/jsondata"; +import { ISite, ISitesGroup } from "./geom"; + +export type ObjDataType = ISite | ISitesGroup | JsonData; + +export interface IobjObs { + properties: ObjDataType; + endPoint: endPoints; + objectType: "site" | "sites_group"; + label: string; + addObjLabel: string; + editObjLabel: string; + id: string | null; + moduleCode: string; + schema: JsonData; + template: { + fieldNames: []; + fieldLabels: JsonData; + fieldNamesList: []; + fieldDefinitions: {}; + }; + dataTable: { colNameObj: {} }; +} diff --git a/frontend/app/services/api-geom.service.ts b/frontend/app/services/api-geom.service.ts index 296913ebf..47fbdd4ae 100644 --- a/frontend/app/services/api-geom.service.ts +++ b/frontend/app/services/api-geom.service.ts @@ -1,83 +1,152 @@ -import { Injectable } from "@angular/core"; -import { Observable } from "rxjs"; -import { GeoJSON } from "geojson"; - -import { CacheService } from "./cache.service"; -import { IGeomService, ISitesGroup, ISite } from "../interfaces/geom"; -import { IPaginated } from "../interfaces/page"; -import { JsonData } from "../types/jsondata"; -import { Resp } from "../types/response"; - -export enum endPoints { - sites_groups = "sites_groups", - sites = "sites", -} +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; + +import { endPoints } from '../enum/endpoints'; +import { IGeomService, ISite, ISiteType, ISitesGroup } from '../interfaces/geom'; +import { IobjObs, ObjDataType } from '../interfaces/objObs'; +import { IPaginated } from '../interfaces/page'; +import { JsonData } from '../types/jsondata'; +import { Resp } from '../types/response'; +import { Utils } from '../utils/utils'; +import { CacheService } from './cache.service'; +import { ConfigJsonService } from './config-json.service'; @Injectable() export class ApiGeomService implements IGeomService { - public objectType: endPoints = endPoints.sites_groups; + public endPoint: endPoints; + public objectObs: IobjObs; - constructor(protected _cacheService: CacheService) { - this.init(); + constructor( + protected _cacheService: CacheService, + protected _configJsonService: ConfigJsonService + ) { + this.init(this.endPoint, this.objectObs); } - init() { - this.objectType = endPoints.sites_groups; + init(endPoint, objectObjs) { + this.endPoint = endPoint; + this.objectObs = objectObjs; + // this.endPoint = endPoints.sites_groups; + // this.objectObs = { + // properties: {}, + // endPoint: endPoints.sites_groups, + // objectType: 'sites_group', + // label: 'groupe de site', + // addObjLabel: 'Ajouter', + // editObjLabel: 'Editer', + // id: null, + // moduleCode: 'generic', + // schema: {}, + // template: { + // fieldNames: [], + // fieldLabels: {}, + // fieldNamesList: [], + // fieldDefinitions: {}, + // }, + // dataTable: { colNameObj: {} }, + // }; } get( page: number = 1, limit: number = 10, params: JsonData = {} ): Observable> { - return this._cacheService.request< - Observable> - >("get", this.objectType, { - queryParams: { page, limit, ...params }, - }); + return this._cacheService.request>>( + 'get', + this.endPoint, + { + queryParams: { page, limit, ...params }, + } + ); } getById(id: number): Observable { return this._cacheService.request>( - "get", - `${this.objectType}/${id}` + 'get', + `${this.endPoint}/${id}` ); } get_geometries(params: JsonData = {}): Observable { return this._cacheService.request>( - "get", - `${this.objectType}/geometries`, + 'get', + `${this.endPoint}/geometries`, { queryParams: { ...params }, } ); } - patch(id: number, updatedData: ISitesGroup | ISite): Observable { - return this._cacheService.request("patch", `${this.objectType}/${id}`, { + patch(id: number, updatedData: { properties: ISitesGroup | ISite }): Observable { + return this._cacheService.request('patch', `${this.endPoint}/${id}`, { postData: updatedData, }); } - create( postData: ISitesGroup | ISite): Observable { - return this._cacheService.request("post", `${this.objectType}`, { + create(postData: { properties: ISitesGroup | ISite }): Observable { + return this._cacheService.request('post', `${this.endPoint}`, { postData: postData, }); } delete(id: number): Observable { - return this._cacheService.request("delete", `${this.objectType}/${id}`); + return this._cacheService.request('delete', `${this.endPoint}/${id}`); } - } @Injectable() export class SitesGroupService extends ApiGeomService { - constructor(_cacheService: CacheService) { - super(_cacheService); + constructor(_cacheService: CacheService, _configJsonService: ConfigJsonService) { + super(_cacheService, _configJsonService); } init(): void { - this.objectType = endPoints.sites_groups; + this.endPoint = endPoints.sites_groups; + this.objectObs = { + properties: {}, + endPoint: endPoints.sites_groups, + objectType: 'sites_group', + label: 'groupe de site', + addObjLabel: 'Ajouter un nouveau groupe de site', + editObjLabel: 'Editer le groupe de site', + id: null, + moduleCode: 'generic', + schema: {}, + template: { + fieldNames: [], + fieldLabels: {}, + fieldNamesList: [], + fieldDefinitions: {}, + }, + dataTable: { colNameObj: {} }, + }; + this._configJsonService + .init(this.objectObs.moduleCode) + .pipe() + .subscribe(() => { + const fieldNames = this._configJsonService.configModuleObjectParam( + this.objectObs.moduleCode, + this.objectObs.objectType, + 'display_properties' + ); + const fieldNamesList = this._configJsonService.configModuleObjectParam( + this.objectObs.moduleCode, + this.objectObs.objectType, + 'display_list' + ); + const schema = this._configJsonService.schema( + this.objectObs.moduleCode, + this.objectObs.objectType + ); + const fieldLabels = this._configJsonService.fieldLabels(schema); + const fieldDefinitions = this._configJsonService.fieldDefinitions(schema); + this.objectObs.template.fieldNames = fieldNames; + this.objectObs.template.fieldNamesList = fieldNamesList; + this.objectObs.schema = schema; + this.objectObs.template.fieldLabels = fieldLabels; + this.objectObs.template.fieldDefinitions = fieldDefinitions; + this.objectObs.template.fieldNamesList = fieldNamesList; + this.objectObs.dataTable.colNameObj = Utils.toObject(fieldNamesList, fieldLabels); + }); } getSitesChild( @@ -85,37 +154,105 @@ export class SitesGroupService extends ApiGeomService { limit: number = 10, params: JsonData = {} ): Observable> { - return this._cacheService.request>>( - "get", - `sites`, - { - queryParams: { page, limit, ...params }, - } - ); + return this._cacheService.request>>('get', `sites`, { + queryParams: { page, limit, ...params }, + }); } addObjectType(): string { - return "un nouveau groupe de site"; + return 'un nouveau groupe de site'; } editObjectType(): string { - return "le groupe de site"; + return 'le groupe de site'; } } @Injectable() export class SitesService extends ApiGeomService { - constructor(_cacheService: CacheService) { - super(_cacheService); + constructor(_cacheService: CacheService, _configJsonService: ConfigJsonService) { + super(_cacheService, _configJsonService); } + opts = []; + init(): void { - this.objectType = endPoints.sites; + this.endPoint = endPoints.sites; + this.objectObs = { + properties: {}, + endPoint: endPoints.sites, + objectType: 'site', + label: 'site', + addObjLabel: 'Ajouter un nouveau site', + editObjLabel: 'Editer le site', + id: null, + moduleCode: 'generic', + schema: {}, + template: { + fieldNames: [], + fieldLabels: {}, + fieldNamesList: [], + fieldDefinitions: {}, + }, + dataTable: { colNameObj: {} }, + }; + this._configJsonService + .init(this.objectObs.moduleCode) + .pipe() + .subscribe(() => { + const fieldNames = this._configJsonService.configModuleObjectParam( + this.objectObs.moduleCode, + this.objectObs.objectType, + 'display_properties' + ); + //FIXME: same as site group: to refact + const fieldNamesList = this._configJsonService.configModuleObjectParam( + this.objectObs.moduleCode, + this.objectObs.objectType, + 'display_list' + ); + const schema = this._configJsonService.schema( + this.objectObs.moduleCode, + this.objectObs.objectType + ); + const fieldLabels = this._configJsonService.fieldLabels(schema); + const fieldDefinitions = this._configJsonService.fieldDefinitions(schema); + this.objectObs.template.fieldNames = fieldNames; + this.objectObs.template.fieldNamesList = fieldNamesList; + this.objectObs.schema = schema; + this.objectObs.template.fieldLabels = fieldLabels; + this.objectObs.template.fieldDefinitions = fieldDefinitions; + this.objectObs.template.fieldNamesList = fieldNamesList; + this.objectObs.dataTable.colNameObj = Utils.toObject(fieldNamesList, fieldLabels); + }); } + + // getTypeSites( + // ): Observable> { + // return this._cacheService.request>>( + // "get", + // "sites/types" + // ); + // } + + getTypeSites( + page: number = 1, + limit: number = 10, + params: JsonData = {} + ): Observable> { + return this._cacheService.request>>( + 'get', + 'sites/types/label', + { + queryParams: { page, limit, ...params }, + } + ); + } + addObjectType(): string { - return " un nouveau site"; + return ' un nouveau site'; } editObjectType(): string { - return "le site"; + return 'le site'; } } diff --git a/frontend/app/services/config-json.service.ts b/frontend/app/services/config-json.service.ts new file mode 100644 index 000000000..282910bd6 --- /dev/null +++ b/frontend/app/services/config-json.service.ts @@ -0,0 +1,60 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { ModuleService } from '@geonature/services/module.service'; +import { AppConfig } from '@geonature_config/app.config'; +import { of } from 'rxjs'; +import { ConfigService } from './config.service'; + + +@Injectable() +export class ConfigJsonService extends ConfigService { + + constructor(_http: HttpClient, _moduleService: ModuleService) { + super(_http, _moduleService) + } + + /** Configuration */ + + init(moduleCode: string) { + if (this._config && this._config[moduleCode]) { + return of(true); + } else { + return this.loadConfig(moduleCode); + } + } + + /** Backend Module Url */ + backendModuleUrl() { + // Test if api endpoint have a final slash + let api_url = AppConfig.API_ENDPOINT; + if (api_url.substring(api_url.length - 1, 1) !== '/') { + api_url = api_url + '/'; + } + return `${api_url}${this._moduleService.currentModule.module_path}`; + } + + fieldLabels(schema) { + const fieldLabels = {}; + for (const key of Object.keys(schema)) { + fieldLabels[key] = schema[key]['attribut_label']; + } + return fieldLabels; + } + + fieldNames(moduleCode, objectType, typeDisplay = '') { + if (['display_properties', 'display_list'].includes(typeDisplay)) { + return this.configModuleObjectParam(moduleCode, objectType, typeDisplay); + } + if (typeDisplay === 'schema') { + return Object.keys(this.schema(moduleCode, objectType)); + } + } + + fieldDefinitions(schema) { + const fieldDefinitions = {}; + for (const key of Object.keys(schema)) { + fieldDefinitions[key] = schema[key]['definition']; + } + return fieldDefinitions; + } +} diff --git a/frontend/app/services/config.service.ts b/frontend/app/services/config.service.ts index 15f5c4ec9..e3470d18a 100644 --- a/frontend/app/services/config.service.ts +++ b/frontend/app/services/config.service.ts @@ -7,17 +7,13 @@ import { ConfigService as GnConfigService } from '@geonature/services/config.ser @Injectable() export class ConfigService { - private _config; + protected _config; - constructor( - private _http: HttpClient, - private _moduleService: ModuleService, - public appConfig: GnConfigService - ) {} + constructor(protected _http: HttpClient, protected _moduleService: ModuleService, public appConfig: GnConfigService) {} /** Configuration */ - init(moduleCode = null) { + init(moduleCode: null | string = null) { // a definir ailleurs moduleCode = moduleCode || 'generic'; @@ -139,17 +135,17 @@ export class ConfigService { /** Config Object Schema */ schema(moduleCode, objectType, typeSchema = 'all'): Object { moduleCode = moduleCode || 'generic'; - const configObject = this._config[moduleCode][objectType]; // gerer quand les paramètres ont un fonction comme valeur - - for (const typeSchema of ['generic', 'specific']) { - for (const keyDef of Object.keys(configObject[typeSchema])) { - const formDef = configObject[typeSchema][keyDef]; - for (const keyParam of Object.keys(formDef)) { - const func = this.toFunction(formDef[keyParam]); - if (func) { - formDef[keyParam] = func; + if (configObject) { + for (const typeSchema of ['generic', 'specific']) { + for (const keyDef of Object.keys(configObject[typeSchema])) { + const formDef = configObject[typeSchema][keyDef]; + for (const keyParam of Object.keys(formDef)) { + const func = this.toFunction(formDef[keyParam]); + if (func) { + formDef[keyParam] = func; + } } } } diff --git a/frontend/app/services/data-table.service.ts b/frontend/app/services/data-table.service.ts index 8a76099ee..f4e28d72e 100644 --- a/frontend/app/services/data-table.service.ts +++ b/frontend/app/services/data-table.service.ts @@ -1,6 +1,7 @@ -import { Injectable } from "@angular/core"; -import { IColumn } from "../interfaces/column"; -import { BehaviorSubject} from "rxjs"; +import { Injectable } from '@angular/core'; + +import { IColumn } from '../interfaces/column'; + interface ItemObjectTable { id: number | null; selected: boolean; @@ -17,13 +18,11 @@ export class DataTableService { rowStatus: ItemObjectTable; idObj: number; - // IF prefered observable compare to ngOnChanges uncomment this: // dataCol:IColumn[] =[{prop:"",name:"",description:""}] // private dataCols = new BehaviorSubject(this.dataCol); // currentCols = this.dataCols.asObservable(); - constructor() {} // IF prefered observable compare to ngOnChanges uncomment this: @@ -39,7 +38,7 @@ export class DataTableService { // this.dataCols.next(allColumn) // } - colsTable(colName:IColumn[], dataTable): IColumn[] { + colsTable(colName: {}, dataTable): IColumn[] { const arr = Object.keys(colName); const allColumn: IColumn[] = arr .filter((item) => Object.keys(dataTable).includes(item)) diff --git a/frontend/app/services/edit-object.service.ts b/frontend/app/services/edit-object.service.ts deleted file mode 100644 index fc106678c..000000000 --- a/frontend/app/services/edit-object.service.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Injectable } from "@angular/core"; -import { BehaviorSubject, Observable, of, forkJoin } from "rxjs"; -import { concatMap } from "rxjs/operators"; - -import { JsonData } from "../types/jsondata"; - -import { Utils } from "../utils/utils"; -import { MonitoringObjectService } from "./monitoring-object.service"; - -@Injectable() -export class EditObjectService { - data: JsonData = {}; - private dataSub = new BehaviorSubject(this.data); - currentData = this.dataSub.asObservable(); - properties: JsonData; - moduleCode:string; - objecType:string; - - constructor( - private _objService:MonitoringObjectService - ) {} - - changeDataSub(newDat: JsonData) { - this.properties = newDat; - newDat.moduleCode = "generic"; - newDat.objectType = "sites_group"; - this.moduleCode= "generic"; - this.objecType= "sites_group" - this.dataSub.next(newDat) - - } - - - - formValues(obj): Observable { - const properties = Utils.copy(this.properties); - const observables = {}; - const schema = obj[this.moduleCode]; - for (const attribut_name of Object.keys(schema)) { - const elem = schema[attribut_name]; - if (!elem.type_widget) { - continue; - } - observables[attribut_name] = this._objService.toForm(elem, properties[attribut_name]); - } - - return forkJoin(observables).pipe( - concatMap((formValues_in) => { - const formValues = Utils.copy(formValues_in); - // geometry - // if (this.config["geometry_type"]) { - // formValues["geometry"] = this.geometry; // copy??? - // } - return of(formValues); - }) - ); - } -} diff --git a/frontend/app/services/form.service.ts b/frontend/app/services/form.service.ts new file mode 100644 index 000000000..0499fccad --- /dev/null +++ b/frontend/app/services/form.service.ts @@ -0,0 +1,105 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable, forkJoin, of } from 'rxjs'; +import { concatMap } from 'rxjs/operators'; + +import { IobjObs, ObjDataType } from '../interfaces/objObs'; +import { ISite, ISitesGroup } from '../interfaces/geom'; +import { JsonData } from '../types/jsondata'; +import { Utils } from '../utils/utils'; +import { MonitoringObjectService } from './monitoring-object.service'; + +@Injectable() +export class FormService { + data: JsonData = {}; + private dataSub = new BehaviorSubject(this.data); + currentData = this.dataSub.asObservable(); + properties: JsonData = {}; + moduleCode: string; + objecType: string; + + constructor(private _objService: MonitoringObjectService) {} + + // TODO: voir si nécessaire de garder ça (objService permet d'avoir le bon objet ? et sinon modifier pour obtenir ce qu'il faut en formulaire) + changeDataSub(newDat: JsonData, objectType: string,endPoint:string,objSelected:IobjObs, moduleCode: string = 'generic') { + this.properties = newDat; + newDat.moduleCode = moduleCode; + newDat.objectType = objectType; + newDat.endPoint = endPoint; + newDat.objSelect = objSelected + this.dataSub.next(newDat); + } + + dataToCreate(newDat: JsonData, moduleCode: string = 'generic') { + newDat[moduleCode] = {}; + newDat.moduleCode = moduleCode; + this.dataSub.next(newDat); + } + + formValues(obj): Observable { + const properties = Utils.copy(this.properties); + const observables = {}; + const schema = obj[obj.moduleCode]; + for (const attribut_name of Object.keys(schema)) { + const elem = schema[attribut_name]; + if (!elem.type_widget) { + continue; + } + observables[attribut_name] = this._objService.toForm(elem, properties[attribut_name]); + } + + return forkJoin(observables).pipe( + concatMap((formValues_in) => { + const formValues = Utils.copy(formValues_in); + // geometry + // if (this.config["geometry_type"]) { + // formValues["geometry"] = this.geometry; // copy??? + // } + return of(formValues); + }) + ); + } + + // TODO: A voir si nécessaire d'utiliser le formatage des post et update data avant éxécution route coté backend + postData(formValue, obj): { properties: ISitesGroup | ISite | any } { + const propertiesData = {}; + const schema = obj[obj.moduleCode]; + for (const attribut_name of Object.keys(schema)) { + const elem = schema[attribut_name]; + if (!elem.type_widget) { + continue; + } + propertiesData[attribut_name] = this._objService.fromForm(elem, formValue[attribut_name]); + } + const postData = { properties: {} }; + if (obj.dataComplement == undefined) { + postData['properties'] = propertiesData; + } else { + postData['properties'] = propertiesData; + postData['dataComplement'] = obj.dataComplement; + } + + // Ajout des id relationship + if (obj.id_relationship != undefined) { + for (const [key, value] of Object.entries(obj.id_relationship)) { + if (typeof value == 'string') { + if (obj[value] != undefined) { + postData['properties'][value] = obj[value]; + } else if (Object.keys(obj.dataComplement).includes(value)) { + postData['properties'][value] = obj.dataComplement[value]; + } + } + } + } + + // properties: propertiesData, + // // id_parent: this.parentId + // }; + + // TODO: A voir q'il faut remettre + // if (this.config["geometry_type"]) { + // postData["geometry"] = formValue["geometry"]; + // postData["type"] = "Feature"; + // } + return postData; + } +} diff --git a/frontend/app/services/object.service.ts b/frontend/app/services/object.service.ts index c570f506c..58b183f7b 100644 --- a/frontend/app/services/object.service.ts +++ b/frontend/app/services/object.service.ts @@ -1,23 +1,53 @@ import { Injectable } from "@angular/core"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject,ReplaySubject } from "rxjs"; +import { endPoints } from "../enum/endpoints"; +import { ISitesGroup, ISiteType } from "../interfaces/geom"; +import { IobjObs, ObjDataType } from "../interfaces/objObs"; +import { JsonData } from "../types/jsondata"; + @Injectable() export class ObjectService { - objectType: string = ""; - private dataObjType = new BehaviorSubject(this.objectType); + objObs: IobjObs; + private objSelected = new ReplaySubject(1); + currentObjSelected = this.objSelected.asObservable(); + + private dataObjType = new ReplaySubject>(1); currentObjectType = this.dataObjType.asObservable(); - - objectTypeParent: string = ""; - private dataObjTypeParent = new BehaviorSubject(this.objectTypeParent); + + private dataObjTypeParent = new ReplaySubject>(1); currentObjectTypeParent = this.dataObjTypeParent.asObservable(); - constructor() {} + constructor() { + let storedObjectType = localStorage.getItem('storedObjectType'); + let storedObjectTypeParent = localStorage.getItem('storedObjectTypeParent'); + let storedObjectSelected= localStorage.getItem('storedObjectSelected'); + if (storedObjectType) + this.changeObjectType(JSON.parse(storedObjectType), false); + + if (storedObjectTypeParent) + this.changeObjectTypeParent(JSON.parse(storedObjectTypeParent), false); + + if (storedObjectSelected) + this.changeSelectedObj(JSON.parse(storedObjectSelected), false); +} + + + changeObjectType(newObjType: IobjObs,storeObjectType: boolean = false) { + if (storeObjectType) + localStorage.setItem('storedObjectType', JSON.stringify(newObjType)); + this.dataObjType.next(newObjType); + } - changeObjectType(newObjType: string) { - this.dataObjType.next(newObjType); + changeObjectTypeParent(newObjType: IobjObs,storeObjectTypeParent: boolean = false) { + if (storeObjectTypeParent) + localStorage.setItem('storedObjectTypeParent', JSON.stringify(newObjType)); + this.dataObjTypeParent.next(newObjType); } - changeObjectTypeParent(newObjType: string) { - this.dataObjTypeParent.next(newObjType); + changeSelectedObj(newObjSelected:ObjDataType , storeObjectTypeSelected: boolean = false ){ + if (storeObjectTypeSelected) + localStorage.setItem('storedObjectSelected', JSON.stringify(newObjSelected)); + this.objSelected.next(newObjSelected); } } diff --git a/frontend/app/utils/utils.ts b/frontend/app/utils/utils.ts index ef5e976b0..f0eb95d69 100644 --- a/frontend/app/utils/utils.ts +++ b/frontend/app/utils/utils.ts @@ -58,4 +58,12 @@ export class Utils { }); return dictOut; } + + static toObject(keys,values) { + const obj = keys.reduce((accumulator, key, index) => { + return {...accumulator, [key]: values[key]}; + }, {}); + + return obj; + } } From bc330085b9a838a31adeda29f3d3a25532a1b6d1 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Fri, 14 Apr 2023 16:02:57 +0200 Subject: [PATCH 031/161] Feat/prepare edit site component & some visits (#43) * refactor: object.service with observable obj Change the objectType string to objectType obj in order to subscribe to multilple properties link to this object Reviewed-by: andriacap [Refs_ticket]: #40 * feat: adapt service to get config json Get fieldsName and fieldsLabel for display properties (properties component) Add logic to setItem in LocalStorage to keepLast Value of observable on reload page TODO: - [ ] see if Localstorage is really necessary with the configService used inside api-geom.service ) - [ ] adapt monitoring-form-g.component with the new service Reviewed-by: andriacap [Refs ticket]: #4 * feat: column datatable from config json Add logic into sitegroups service in order to use config json to diplay column datatable (called "display_list") Review-by: andriac [Refs ticket]: #4 * feat: adjust backend to load config json Adjust backend code to use existing code in order to load config from file json and by starting with "sites_group" Fix Media load and upload for site_group TODO: - [ ] check if config should be find from file or backend - [ ] Optimize logic backend (use generic model with class method ?) Reviewed-by: andriacap [Refs ticket]: #4 * feat: add button multiselect with filter on backend filter with params backend (new route -> see if it's possible to change that) Add button multiselect above form type site TODO: - improve Input / condition of use case of btn multiselect Reviewed-by: andriac * feat: btn multiselect option -Add @Input fn , placeholder, title, paramsToFilt -Remove empty option -prevent adding from keyboard input or not including in list -Store config json into object Reviewed-by: @andriac [Refs-ticket]: #4 * refactor: change form-service and object-service refactor form-service add observable into object-service (WIP: futur use for refresh page ?) Rieviewed-by: andriac [Refs_ticket]: #40 * refactor: test refresh page and comportment obs refresh page seems to works with localstorage of different obj in object_service Reviewed-by: andriac * feat: dynamic form - Add: - pass config json to form.component-g.ts - add config json to this.obj and refresh form into form.component-g.ts - add css for form component to deal when long form selected -fix : - refresh page form component (this._configService.init is necessary ...) - comportment different between add or edit into form component and form service Reviewed-by: andriac [Refs_ticket]: #4 * feat: dynamic form - Correction PR Remove unused console.log Rxjs : use concatMap to avoid subscribe inside subscribe Apply: prettier and sort prettier for import ts file Reviewed-by: @andriac [Refs_PR]: #42 * feat: dynamic create site - Add change current-object when click "Add " from datatable component - api-geom.service: change the way to init the api-geom - WIP: two subscribes in one .. (the subscription is call only once ..) - WIP : get config json from backend Reviewed-by: andriac * feat: add relationship id into sendData form site Add type_site and id_site_group to the form site Reviewed-by: andriac [Refs_ticket]: #5 et #6 * feat(back): add custom config when post site Change backend files in order to choose specific config when site is created. Reviewed-by: andriac [Refs_ticket]: #5 , #6 * fix: problem of type object when loading form Add endPoint and objectType to the observable using by the formservice in order to use _apiGeomService with good context (endPoint) [Refs_ticket]: #5 et #6 * fix: tooltip and label inside datatable-component Add a childLabel inside interface objObs.ts in order to use this tooltip inside datatable-component * feat: get back work visitcomponent get back work from feat/visit to use visitcomponent but adapted to current branch [Refs_ticket]: #5 , #6 * fix: forgot 3 modifications needed from feat/site Add conftest visit Add test_get_visits Add get_site_by_id * fix: passing data between components and property - Action button "consult..." is working for group_site and site - Action with button "cancel" , "come back history routing" send to the right component - Label and tooltip are according to the parent and child object loaded in the corresponding component - Action "Add site" directly from datatable-g.component (WIP: need to remove send to sites_group/:id/create [Refs_ticket]: #40 , #4 , #5 and #6 * fix: error of url relative when using form Fix problem of id_module has ["id_module"] in create_or_update_object_api function Fix path "sites/:id" to "site/:id" Add urlRelative when editing component * fix: problem route navigation Remove unused "urlRelative" create in last commit Fix the action "add children" in datatable to redirect to the good component Change the way to redirect when cancel and delete on form component based on last url [Refs_ticket]: #40 * refactor: object.service with observable obj Change the objectType string to objectType obj in order to subscribe to multilple properties link to this object Reviewed-by: andriacap [Refs_ticket]: #40 * feat: adapt service to get config json Get fieldsName and fieldsLabel for display properties (properties component) Add logic to setItem in LocalStorage to keepLast Value of observable on reload page TODO: - [ ] see if Localstorage is really necessary with the configService used inside api-geom.service ) - [ ] adapt monitoring-form-g.component with the new service Reviewed-by: andriacap [Refs ticket]: #4 * feat: column datatable from config json Add logic into sitegroups service in order to use config json to diplay column datatable (called "display_list") Review-by: andriac [Refs ticket]: #4 * feat: adjust backend to load config json Adjust backend code to use existing code in order to load config from file json and by starting with "sites_group" Fix Media load and upload for site_group TODO: - [ ] check if config should be find from file or backend - [ ] Optimize logic backend (use generic model with class method ?) Reviewed-by: andriacap [Refs ticket]: #4 * feat: add button multiselect with filter on backend filter with params backend (new route -> see if it's possible to change that) Add button multiselect above form type site TODO: - improve Input / condition of use case of btn multiselect Reviewed-by: andriac * feat: btn multiselect option -Add @Input fn , placeholder, title, paramsToFilt -Remove empty option -prevent adding from keyboard input or not including in list -Store config json into object Reviewed-by: @andriac [Refs-ticket]: #4 * refactor: change form-service and object-service refactor form-service add observable into object-service (WIP: futur use for refresh page ?) Rieviewed-by: andriac [Refs_ticket]: #40 * refactor: test refresh page and comportment obs refresh page seems to works with localstorage of different obj in object_service Reviewed-by: andriac * feat: dynamic form - Add: - pass config json to form.component-g.ts - add config json to this.obj and refresh form into form.component-g.ts - add css for form component to deal when long form selected -fix : - refresh page form component (this._configService.init is necessary ...) - comportment different between add or edit into form component and form service Reviewed-by: andriac [Refs_ticket]: #4 * feat: dynamic form - Correction PR Remove unused console.log Rxjs : use concatMap to avoid subscribe inside subscribe Apply: prettier and sort prettier for import ts file Reviewed-by: @andriac [Refs_PR]: #42 * feat: dynamic create site - Add change current-object when click "Add " from datatable component - api-geom.service: change the way to init the api-geom - WIP: two subscribes in one .. (the subscription is call only once ..) - WIP : get config json from backend Reviewed-by: andriac * feat: add relationship id into sendData form site Add type_site and id_site_group to the form site Reviewed-by: andriac [Refs_ticket]: #5 et #6 * feat(back): add custom config when post site Change backend files in order to choose specific config when site is created. Reviewed-by: andriac [Refs_ticket]: #5 , #6 * fix: tooltip and label inside datatable-component Add a childLabel inside interface objObs.ts in order to use this tooltip inside datatable-component * feat: get back work visitcomponent get back work from feat/visit to use visitcomponent but adapted to current branch [Refs_ticket]: #5 , #6 * fix: forgot 3 modifications needed from feat/site Add conftest visit Add test_get_visits Add get_site_by_id * fix: passing data between components and property - Action button "consult..." is working for group_site and site - Action with button "cancel" , "come back history routing" send to the right component - Label and tooltip are according to the parent and child object loaded in the corresponding component - Action "Add site" directly from datatable-g.component (WIP: need to remove send to sites_group/:id/create [Refs_ticket]: #40 , #4 , #5 and #6 * fix: error of url relative when using form Fix problem of id_module has ["id_module"] in create_or_update_object_api function Fix path "sites/:id" to "site/:id" Add urlRelative when editing component * fix: fix things broken by rebase * chore: remove useless file * chore(front): remove file/comment/console.log --------- Co-authored-by: Andria Capai --- .../config/generic/site.json | 11 +-- .../gn_module_monitoring/monitoring/models.py | 20 +++- .../monitoring/schemas.py | 17 +++- backend/gn_module_monitoring/routes/site.py | 6 ++ .../routes/sites_groups.py | 44 +++++++++ backend/gn_module_monitoring/routes/visit.py | 35 +++++++ .../gn_module_monitoring/tests/conftest.py | 1 + .../tests/fixtures/visit.py | 27 ++++++ .../tests/test_routes/test_visit.py | 29 ++++++ frontend/app/class/monitoring-visit.ts | 6 ++ .../monitoring-datatable-g.component.html | 6 +- .../monitoring-datatable-g.component.ts | 18 +++- .../monitoring-form.component-g.ts | 33 ++++--- .../monitoring-properties-g.component.ts | 64 +++++++------ .../monitoring-sites-create.component.ts | 43 ++++++--- .../monitoring-sites-edit.component.css | 0 .../monitoring-sites-edit.component.html | 13 +++ .../monitoring-sites-edit.component.ts | 73 +++++++++++++++ .../monitoring-sites.component.html | 1 + .../monitoring-sites.component.ts | 36 ++++---- ...monitoring-sitesgroups-create.component.ts | 40 +++++--- .../monitoring-sitesgroups.component.ts | 1 - .../monitoring-visits.component.css | 0 .../monitoring-visits.component.html | 8 ++ .../monitoring-visits.component.ts | 92 +++++++++++++++++++ frontend/app/enum/endpoints.ts | 1 + frontend/app/gnModule.module.ts | 19 +++- frontend/app/interfaces/geom.ts | 6 ++ frontend/app/interfaces/objObs.ts | 7 +- frontend/app/interfaces/object.ts | 20 ++++ frontend/app/interfaces/visit.ts | 18 ++++ frontend/app/services/api-geom.service.ts | 77 +++++++++++++++- frontend/app/services/config-json.service.ts | 1 + frontend/app/services/form.service.ts | 14 ++- 34 files changed, 660 insertions(+), 127 deletions(-) create mode 100644 backend/gn_module_monitoring/routes/visit.py create mode 100644 backend/gn_module_monitoring/tests/fixtures/visit.py create mode 100644 backend/gn_module_monitoring/tests/test_routes/test_visit.py create mode 100644 frontend/app/class/monitoring-visit.ts create mode 100644 frontend/app/components/monitoring-sites-edit/monitoring-sites-edit.component.css create mode 100644 frontend/app/components/monitoring-sites-edit/monitoring-sites-edit.component.html create mode 100644 frontend/app/components/monitoring-sites-edit/monitoring-sites-edit.component.ts create mode 100644 frontend/app/components/monitoring-visits/monitoring-visits.component.css create mode 100644 frontend/app/components/monitoring-visits/monitoring-visits.component.html create mode 100644 frontend/app/components/monitoring-visits/monitoring-visits.component.ts create mode 100644 frontend/app/interfaces/object.ts create mode 100644 frontend/app/interfaces/visit.ts diff --git a/backend/gn_module_monitoring/config/generic/site.json b/backend/gn_module_monitoring/config/generic/site.json index 01c741d1f..2b374a431 100644 --- a/backend/gn_module_monitoring/config/generic/site.json +++ b/backend/gn_module_monitoring/config/generic/site.json @@ -58,13 +58,6 @@ "type_util": "user", "required": true }, - "id_digitiser": { - "type_widget": "text", - "attribut_label": "Numérisateur", - "required": true, - "hidden": true, - "type_util": "user" - }, "first_use_date": { "type_widget": "date", "attribut_label": "Date description", @@ -89,11 +82,11 @@ "type_widget": "integer", "attribut_label": "Altitude (min)" }, - "altitude_max": { + "altitude_max": { "type_widget": "integer", "attribut_label": "Altitude (max)" }, - "id_sites_group": { + "id_sites_group": { "type_widget": "integer", "attribut_label": "ID Sites Groups", "hidden": true, diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index 9ea917069..a4f3493c1 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -171,13 +171,13 @@ class TMonitoringObservations(TObservations): @serializable -class TMonitoringVisits(TBaseVisits): +class TMonitoringVisits(TBaseVisits, GenericModel): __tablename__ = "t_visit_complements" __table_args__ = {"schema": "gn_monitoring"} __mapper_args__ = { "polymorphic_identity": "monitoring_visit", } - + query_class = MonitoringQuery id_base_visit = DB.Column( DB.ForeignKey("gn_monitoring.t_base_visits.id_base_visit"), nullable=False, @@ -210,10 +210,20 @@ class TMonitoringVisits(TBaseVisits): ) + module = DB.relationship( + TModules, + lazy="select", + primaryjoin=(TModules.id_module == TBaseVisits.id_module), + foreign_keys=[TModules.id_module], + uselist=False, + ) + + @geoserializable(geoCol="geom", idCol="id_base_site") -class TMonitoringSites(TBaseSites): - __tablename__ = "t_site_complements" - __table_args__ = {"schema": "gn_monitoring"} +class TMonitoringSites(TBaseSites, GenericModel): + + __tablename__ = 't_site_complements' + __table_args__ = {'schema': 'gn_monitoring'} __mapper_args__ = { "polymorphic_identity": "monitoring_site", } diff --git a/backend/gn_module_monitoring/monitoring/schemas.py b/backend/gn_module_monitoring/monitoring/schemas.py index fcbbc76d3..7dde989c3 100644 --- a/backend/gn_module_monitoring/monitoring/schemas.py +++ b/backend/gn_module_monitoring/monitoring/schemas.py @@ -3,12 +3,13 @@ import geojson from geonature.utils.env import MA from marshmallow import Schema, fields, validate -from geonature.core.gn_commons.schemas import MediaSchema +from geonature.core.gn_commons.schemas import MediaSchema, ModuleSchema from gn_module_monitoring.monitoring.models import ( BibTypeSite, TMonitoringSites, TMonitoringSitesGroups, + TMonitoringVisits ) @@ -51,11 +52,14 @@ class Meta: exclude = ("geom_geojson", "geom") geometry = fields.Method("serialize_geojson", dump_only=True) + pk = fields.Method("set_pk",dump_only=True) def serialize_geojson(self, obj): if obj.geom is not None: return geojson.dumps(obj.as_geofeature().get("geometry")) - + + def set_pk(self,obj): + return self.Meta.model.get_id() class BibTypeSiteSchema(MA.SQLAlchemyAutoSchema): label = fields.Method("get_label_from_type_site") @@ -69,3 +73,12 @@ class Meta: model = BibTypeSite include_fk = True load_instance = True + +class MonitoringVisitsSchema(MA.SQLAlchemyAutoSchema): + class Meta: + model = TMonitoringVisits + pk = fields.Method("set_pk",dump_only=True) + module = MA.Nested(ModuleSchema) + + def set_pk(self,obj): + return self.Meta.model.get_id() diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py index 1bc413785..0e2444b37 100644 --- a/backend/gn_module_monitoring/routes/site.py +++ b/backend/gn_module_monitoring/routes/site.py @@ -6,6 +6,7 @@ from gn_module_monitoring.config.repositories import get_config from gn_module_monitoring.monitoring.models import BibTypeSite, TMonitoringSites, TNomenclatures from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema +from gn_module_monitoring.routes.sites_groups import create_or_update_object_api from gn_module_monitoring.utils.routes import ( create_or_update_object_api_sites_sites_group, filter_params, @@ -85,6 +86,11 @@ def get_sites(): page=page, ) +@blueprint.route("/sites/", methods=["GET"]) +def get_site_by_id(id_base_site): + site = TMonitoringSites.query.get_or_404(id_base_site) + schema = MonitoringSitesSchema() + return schema.dump(site) @blueprint.route("/sites/geometries", methods=["GET"]) def get_all_site_geometries(): diff --git a/backend/gn_module_monitoring/routes/sites_groups.py b/backend/gn_module_monitoring/routes/sites_groups.py index 28888ef30..1c4ed6832 100644 --- a/backend/gn_module_monitoring/routes/sites_groups.py +++ b/backend/gn_module_monitoring/routes/sites_groups.py @@ -103,3 +103,47 @@ def handle_validation_error(error): status_code=422, payload=error.data, ).to_dict() + + +# TODO: OPTIMIZE in order to adapt to new monitoring module (entry by sites_groups) + + +def create_or_update_object_api(module_code, object_type, id=None): + """ + route pour la création ou la modification d'un objet + si id est renseigné, c'est une création (PATCH) + sinon c'est une modification (POST) + + :param module_code: reference le module concerne + :param object_type: le type d'object (site, visit, obervation) + :param id : l'identifiant de l'object (de id_base_site pour site) + :type module_code: str + :type object_type: str + :type id: int + :return: renvoie l'object crée ou modifié + :rtype: dict + """ + depth = to_int(request.args.get("depth", 1)) + + # recupération des données post + post_data = dict(request.get_json()) + if module_code != "generic": + module = get_module("module_code", module_code) + else: + module = {"id_module": "generic"} + #TODO : A enlever une fois que le post_data contiendra geometry et type depuis le front + if object_type == "site": + post_data["geometry"]={'type':'Point', 'coordinates':[2.5,50]} + post_data["type"]='Feature' + # on rajoute id_module s'il n'est pas renseigné par défaut ?? + if "id_module" not in post_data["properties"]: + module["id_module"] = "generic" + post_data["properties"]["id_module"] = module["id_module"] + else: + post_data["properties"]["id_module"] = module["id_module"] + + return ( + monitoring_g_definitions.monitoring_object_instance(module_code, object_type, id) + .create_or_update(post_data) + .serialize(depth) + ) diff --git a/backend/gn_module_monitoring/routes/visit.py b/backend/gn_module_monitoring/routes/visit.py new file mode 100644 index 000000000..4303ba51e --- /dev/null +++ b/backend/gn_module_monitoring/routes/visit.py @@ -0,0 +1,35 @@ +from flask import request +from sqlalchemy.orm import joinedload +from werkzeug.datastructures import MultiDict + +from gn_module_monitoring.blueprint import blueprint +from gn_module_monitoring.monitoring.models import TMonitoringVisits +from gn_module_monitoring.monitoring.schemas import MonitoringVisitsSchema +from gn_module_monitoring.utils.routes import ( + filter_params, + get_limit_page, + get_sort, + paginate, + sort, +) + +# Retrieves visits that do not depend on modules + + +@blueprint.route("/visits", methods=["GET"]) +def get_visits(): + params = MultiDict(request.args) + limit, page = get_limit_page(params=params) + sort_label, sort_dir = get_sort( + params=params, default_sort="id_base_visit", default_direction="desc" + ) + query = TMonitoringVisits.query.options(joinedload(TMonitoringVisits.module)) + query = filter_params(query=TMonitoringVisits.query, params=params) + query = sort(query=query, sort=sort_label, sort_dir=sort_dir) + + return paginate( + query=query, + schema=MonitoringVisitsSchema, + limit=limit, + page=page, + ) diff --git a/backend/gn_module_monitoring/tests/conftest.py b/backend/gn_module_monitoring/tests/conftest.py index 422312d22..92ab61efc 100644 --- a/backend/gn_module_monitoring/tests/conftest.py +++ b/backend/gn_module_monitoring/tests/conftest.py @@ -6,4 +6,5 @@ "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/backend/gn_module_monitoring/tests/fixtures/visit.py b/backend/gn_module_monitoring/tests/fixtures/visit.py new file mode 100644 index 000000000..cd7dc197d --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/visit.py @@ -0,0 +1,27 @@ +import datetime + +import pytest +from geonature.tests.fixtures import datasets +from geonature.utils.env import db + +from gn_module_monitoring.monitoring.models import TMonitoringVisits + + +@pytest.fixture +def visits(module, users, types_site, sites, datasets): + now = datetime.datetime.now() + dataset = datasets["orphan_dataset"] + db_visits = [] + for site in sites.values(): + db_visits.append( + TMonitoringVisits( + id_base_site=site.id_base_site, + id_module=module.id_module, + id_dataset=dataset.id_dataset, + visit_date_min=now, + ) + ) + with db.session.begin_nested(): + db.session.add_all(db_visits) + + return db_visits diff --git a/backend/gn_module_monitoring/tests/test_routes/test_visit.py b/backend/gn_module_monitoring/tests/test_routes/test_visit.py new file mode 100644 index 000000000..67de0f658 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_routes/test_visit.py @@ -0,0 +1,29 @@ +import pytest +from flask import url_for + + +@pytest.mark.usefixtures("client_class", "temporary_transaction") +class TestVisits: + def test_get_visits(self, visits): + r = self.client.get( + url_for( + "monitorings.get_visits", + ) + ) + + expected_visits = {visit.id_base_visit for visit in visits} + current_visits = {visit["id_base_visit"] for visit in r.json["items"]} + assert expected_visits.issubset(current_visits) + assert all(visit["module"] is not None for visit in r.json["items"]) + + def test_get_visits_with_site(self, visits, sites): + site = list(sites.values())[0] + + r = self.client.get(url_for("monitorings.get_visits", id_base_site=site.id_base_site)) + + expected_visits = { + visit.id_base_visit for visit in visits if visit.id_base_site == site.id_base_site + } + current_visits = {visit["id_base_visit"] for visit in r.json["items"]} + + assert expected_visits.issubset(current_visits) diff --git a/frontend/app/class/monitoring-visit.ts b/frontend/app/class/monitoring-visit.ts new file mode 100644 index 000000000..5f34dc629 --- /dev/null +++ b/frontend/app/class/monitoring-visit.ts @@ -0,0 +1,6 @@ +export enum columnNameVisit { + id_module = "Protocol ID", + visit_date_max = "Date max", + visit_date_min = "Date min", + nb_observations = "Nb. observations", +} diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html index 7db5849a0..095b562a1 100644 --- a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html @@ -52,15 +52,15 @@ diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts index 73bb836d8..816fe4e6d 100644 --- a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts @@ -186,12 +186,20 @@ export class MonitoringDatatableGComponent implements OnInit { } } } - navigateToAddChildren(_, rowId) { - this.addEvent.emit(rowId); + navigateToAddChildren(_, row) { + this.addEvent.emit(row); this._objService.changeObjectType(this.objectType); - this.router.navigate(['create'], { - relativeTo: this._Activatedroute, - }); + if (row){ + row['id'] = row[row.pk]; + this.router.navigate([row.id,'create'], { + relativeTo: this._Activatedroute, + }); + } else { + this.router.navigate(['create'], { + relativeTo: this._Activatedroute, + }); + } + } navigateToDetail(row) { row['id'] = row.pk; diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts index 683fbe72f..cf28af17f 100644 --- a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts @@ -60,7 +60,6 @@ export class MonitoringFormComponentG implements OnInit { private _formService: FormService, private _apiGeomService: ApiGeomService, private _router: Router, - private _objService: ObjectService ) {} ngOnInit() { @@ -92,7 +91,6 @@ export class MonitoringFormComponentG implements OnInit { } - // meta pour les parametres dynamiques // ici pour avoir acces aux nomenclatures this.meta = { @@ -261,14 +259,20 @@ export class MonitoringFormComponentG implements OnInit { */ navigateToDetail(id, objectType, queryParams) { // patch bug navigation - this._router.navigate( - ['monitorings', objectType, id].filter((s) => !!s), - { - queryParams, - } - ); + // this._router.navigate( + // ['monitorings', objectType, id].filter((s) => !!s), + // { + // queryParams, + // } + // ); + // TODO: this commented code works only if ".." is not based url (example working : sites_group/:id/site/:id , not working if create site_group) + // this._router.navigate(['..',objectType,id], {relativeTo: this._route}); + // + const urlSegment = [objectType, id].filter((s) => !!s); + const urlPathDetail = [this.obj.urlRelative].concat(urlSegment).join('/'); this.objChanged.emit(this.obj); this.bEditChange.emit(false); + this._router.navigateByUrl(urlPathDetail); } /** @@ -276,11 +280,11 @@ export class MonitoringFormComponentG implements OnInit { */ navigateToParent() { this.bEditChange.emit(false); // patch bug navigation - this._router.navigateByUrl('/monitorings/sites_group'); - - // this.obj.navigateToParent(); + this._router.navigate(['..'], {relativeTo: this._route}); } + + msgToaster(action) { // return `${action} ${this.obj.labelDu()} ${this.obj.description()} effectuée`.trim(); return `${action} effectuée`.trim(); @@ -298,7 +302,10 @@ export class MonitoringFormComponentG implements OnInit { action.subscribe((objData) => { this._commonService.regularToaster('success', this.msgToaster(actionLabel)); this.bSaveSpinner = this.bSaveAndAddChildrenSpinner = false; - // this.objChanged.emit(this.obj); + if (objData.hasOwnProperty('id')) { + this.obj.id = objData['id']; + } + this.objChanged.emit(this.obj); /** si c'est un module : reset de la config */ if (this.obj.objectType === 'module') { @@ -388,6 +395,6 @@ export class MonitoringFormComponentG implements OnInit { } } Object.assign(this.obj.dataComplement, event); - this._formService.dataToCreate(this.obj); + this._formService.dataToCreate(this.obj, this.obj.urlRelative); } } diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts index 0c4cf5456..3c411bf06 100644 --- a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts @@ -1,55 +1,63 @@ -import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core"; -import { FormControl } from "@angular/forms"; -import { ISitesGroup } from "../../interfaces/geom"; -import { IobjObs, ObjDataType } from "../../interfaces/objObs"; -import { FormService } from "../../services/form.service"; -import { ObjectService } from "../../services/object.service"; -import { JsonData } from "../../types/jsondata"; +import { Component, EventEmitter, Input, OnInit, Output, SimpleChanges } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { Subscription } from 'rxjs'; + +import { ISitesGroup } from '../../interfaces/geom'; +import { IobjObs, ObjDataType } from '../../interfaces/objObs'; +import { FormService } from '../../services/form.service'; +import { ObjectService } from '../../services/object.service'; +import { JsonData } from '../../types/jsondata'; @Component({ - selector: "pnx-monitoring-properties-g", - templateUrl: "./monitoring-properties-g.component.html", - styleUrls: ["./monitoring-properties-g.component.css"], + selector: 'pnx-monitoring-properties-g', + templateUrl: './monitoring-properties-g.component.html', + styleUrls: ['./monitoring-properties-g.component.css'], }) export class MonitoringPropertiesGComponent implements OnInit { // selectedObj: ISitesGroup; @Input() selectedObj: ObjDataType; @Input() bEdit: boolean; @Output() bEditChange = new EventEmitter(); - @Input() objectType: IobjObs; + objectType: IobjObs; - color: string = "white"; + @Input() newParentType; + color: string = 'white'; dataDetails: ISitesGroup; fields: JsonData; fieldDefinitions: JsonData; fieldsNames: string[]; - endPoint:string; + endPoint: string; datasetForm = new FormControl(); + _sub: Subscription; - constructor( - private _formService: FormService, - private _objService: ObjectService, - ) {} + constructor(private _formService: FormService, private _objService: ObjectService) {} ngOnInit() { - this._objService.currentObjectTypeParent.subscribe((newObjType) => { - this.objectType = newObjType; - this.fieldsNames = newObjType.template.fieldNames; - this.fields = newObjType.template.fieldLabels; - this.fieldDefinitions = newObjType.template.fieldDefinitions; - this.objectType.properties = this.selectedObj; - this.endPoint = newObjType.endPoint; - }); + this.initProperties(); + } + + initProperties() { + this.objectType = this.newParentType; + this.fieldsNames = this.newParentType.template.fieldNames; + this.fields = this.newParentType.template.fieldLabels; + this.fieldDefinitions = this.newParentType.template.fieldDefinitions; + this.objectType.properties = this.selectedObj; + this.endPoint = this.newParentType.endPoint; } onEditClick() { this.bEditChange.emit(true); - this.selectedObj["id"] = this.selectedObj[this.selectedObj.pk]; + this.selectedObj['id'] = this.selectedObj[this.selectedObj.pk]; this._formService.changeDataSub( this.selectedObj, this.objectType.objectType, - this.objectType.endPoint, - this.objectType + this.objectType.endPoint ); } + + ngOnChanges(changes: SimpleChanges): void { + if (this.newParentType.template.fieldNames.length != 0) { + this.initProperties(); + } + } } diff --git a/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.ts b/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.ts index 32514d56f..33741d789 100644 --- a/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.ts +++ b/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.ts @@ -1,14 +1,16 @@ import { Component, OnInit, ViewChild } from '@angular/core'; -import { FormService } from '../../services/form.service'; -import { FormGroup, FormBuilder } from '@angular/forms'; -import { ISite, ISiteType } from '../../interfaces/geom'; -import { SitesService } from '../../services/api-geom.service'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; + +import { endPoints } from '../../enum/endpoints'; +import { ISite, ISiteType } from '../../interfaces/geom'; import { IobjObs, ObjDataType } from '../../interfaces/objObs'; -import { MonitoringFormComponentG } from '../monitoring-form-g/monitoring-form.component-g'; +import { SitesService } from '../../services/api-geom.service'; +import { FormService } from '../../services/form.service'; import { ObjectService } from '../../services/object.service'; import { JsonData } from '../../types/jsondata'; -import { endPoints } from '../../enum/endpoints'; +import { MonitoringFormComponentG } from '../monitoring-form-g/monitoring-form.component-g'; import { IPaginated } from '../../interfaces/page'; @Component({ @@ -28,30 +30,41 @@ export class MonitoringSitesCreateComponent implements OnInit { @ViewChild('subscritionObjConfig') monitoringFormComponentG: MonitoringFormComponentG; objToCreate: IobjObs; - + urlRelative: string; constructor( private _formService: FormService, private _formBuilder: FormBuilder, private siteService: SitesService, + private route: ActivatedRoute, private _objService: ObjectService ) {} ngOnInit() { + this.urlRelative = this.removeLastPart(this.route.snapshot['_routerState'].url); this._objService.currentObjSelected.subscribe((objParent) => { this.id_sites_group = objParent.id_sites_group; - this._formService.dataToCreate({ - module: 'generic', - objectType: 'site', - id_sites_group: this.id_sites_group, - id_relationship: ['id_sites_group', 'types_site'], - endPoint: endPoints.sites, - objSelected: {}, - }); + this._formService.dataToCreate( + { + module: 'generic', + objectType: 'site', + id: null, + id_sites_group: this.id_sites_group, + id_relationship: ['id_sites_group', 'types_site'], + endPoint: endPoints.sites, + objSelected: objParent.objectType, + }, + this.urlRelative + ); this.form = this._formBuilder.group({}); this.funcToFilt = this.partialfuncToFilt.bind(this); }); } + removeLastPart(url: string): string { + return url.slice(0, url.lastIndexOf('/')); + } + + partialfuncToFilt( pageNumber: number, limit: number, diff --git a/frontend/app/components/monitoring-sites-edit/monitoring-sites-edit.component.css b/frontend/app/components/monitoring-sites-edit/monitoring-sites-edit.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/app/components/monitoring-sites-edit/monitoring-sites-edit.component.html b/frontend/app/components/monitoring-sites-edit/monitoring-sites-edit.component.html new file mode 100644 index 000000000..cc95c530c --- /dev/null +++ b/frontend/app/components/monitoring-sites-edit/monitoring-sites-edit.component.html @@ -0,0 +1,13 @@ + +
+ +
diff --git a/frontend/app/components/monitoring-sites-edit/monitoring-sites-edit.component.ts b/frontend/app/components/monitoring-sites-edit/monitoring-sites-edit.component.ts new file mode 100644 index 000000000..11dc7010a --- /dev/null +++ b/frontend/app/components/monitoring-sites-edit/monitoring-sites-edit.component.ts @@ -0,0 +1,73 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; + +import { ISite } from '../../interfaces/geom'; +import { IobjObs, ObjDataType } from '../../interfaces/objObs'; +import { SitesService } from '../../services/api-geom.service'; +import { FormService } from '../../services/form.service'; +import { ObjectService } from '../../services/object.service'; +import { JsonData } from '../../types/jsondata'; +import { MonitoringFormComponentG } from '../monitoring-form-g/monitoring-form.component-g'; + +@Component({ + selector: 'monitoring-sites-edit', + templateUrl: './monitoring-sites-edit.component.html', + styleUrls: ['./monitoring-sites-edit.component.css'], +}) +export class MonitoringSitesEditComponent implements OnInit { + site: ISite; + form: FormGroup; + paramToFilt: string = 'label'; + funcToFilt: Function; + titleBtn: string = 'Choix des types de sites'; + placeholderText: string = 'Sélectionnez les types de site'; + id_sites_group: number; + types_site: string[]; + @ViewChild('subscritionObjConfig') + monitoringFormComponentG: MonitoringFormComponentG; + objToCreate: IobjObs; + + constructor( + private _formService: FormService, + private _formBuilder: FormBuilder, + private siteService: SitesService, + private _Activatedroute: ActivatedRoute, + private _objService: ObjectService + ) {} + + ngOnInit() { + + this._objService.currentObjSelected.subscribe((objParent) => { + this.id_sites_group = objParent.id_sites_group; + // this._formService.changeDataSub({ module: "generic", objectType: "site", id_sites_group : this.id_sites_group, id_relationship: ['id_sites_group','types_site'],endPoint:endPoints.sites,objSelected:objParent.objectType}); + this.form = this._formBuilder.group({}); + this.funcToFilt = this.partialfuncToFilt.bind(this); + }); + } + + partialfuncToFilt(pageNumber: number, limit: number, valueToFilter: string): Observable { + return this.siteService.getTypeSites(pageNumber, limit, { + label_fr: valueToFilter, + sort_dir: 'desc', + }); + } + + onSendConfig(config: JsonData): void { + config = this.addTypeSiteListIds(config); + this.monitoringFormComponentG.getConfigFromBtnSelect(config); + } + + addTypeSiteListIds(config: JsonData): JsonData { + if (config && config.length != 0) { + config['types_site'] = []; + for (const key in config) { + if ('id_nomenclature_type_site' in config[key]) { + config['types_site'].push(config[key]['id_nomenclature_type_site']); + } + } + } + return config; + } +} diff --git a/frontend/app/components/monitoring-sites/monitoring-sites.component.html b/frontend/app/components/monitoring-sites/monitoring-sites.component.html index 0f495dfae..7c7b3aa9f 100644 --- a/frontend/app/components/monitoring-sites/monitoring-sites.component.html +++ b/frontend/app/components/monitoring-sites/monitoring-sites.component.html @@ -2,6 +2,7 @@ *ngIf="!bEdit" [(bEdit)]="bEdit" [selectedObj]="sitesGroup" + [newParentType]="objParent" > ; + objParent: any; constructor( private _sitesGroupService: SitesGroupService, @@ -53,19 +51,23 @@ export class MonitoringSitesComponent ngOnInit() { this.objForm = this._formBuilder.group({}); - this._objService.changeObjectType(this._siteService.objectObs); + // this._sitesGroupService.init() + this._objService.changeObjectTypeParent(this._sitesGroupService.objectObs, true); + this._objService.currentObjectTypeParent.subscribe((objParent) => { + this.objParent = objParent; + }); + this._objService.changeObjectType(this._siteService.objectObs, true); this.initSite(); } initSite() { this._Activatedroute.params .pipe( - map((params) => params["id"] as number), + map((params) => params['id'] as number), tap((id: number) => { - this._geojsonService.getSitesGroupsChildGeometries( - this.onEachFeatureSite(), - { id_sites_group: id } - ); + this._geojsonService.getSitesGroupsChildGeometries(this.onEachFeatureSite(), { + id_sites_group: id, + }); }), mergeMap((id: number) => forkJoin({ @@ -95,9 +97,7 @@ export class MonitoringSitesComponent ); } ngOnDestroy() { - this._geojsonService.removeFeatureGroup( - this._geojsonService.sitesFeatureGroup - ); + this._geojsonService.removeFeatureGroup(this._geojsonService.sitesFeatureGroup); this._geojsonService.removeFeatureGroup(this.siteGroupLayer); } @@ -107,7 +107,7 @@ export class MonitoringSitesComponent const popup = setPopup( baseUrl, feature.properties.id_base_site, - "Site :" + feature.properties.base_site_name + 'Site :' + feature.properties.base_site_name ); layer.bindPopup(popup); }; @@ -128,7 +128,7 @@ export class MonitoringSitesComponent seeDetails($event) { this._objService.changeObjectTypeParent(this._siteService.objectObs, true); - this.router.navigate([`sites/${$event.id_base_site}`], { + this.router.navigate([`site/${$event.id_base_site}`], { relativeTo: this._Activatedroute, }); } diff --git a/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts index 07c4e379b..3e6b774cc 100644 --- a/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts +++ b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts @@ -1,29 +1,39 @@ -import { Component, OnInit } from "@angular/core"; -import { FormService } from "../../services/form.service"; -import { FormGroup, FormBuilder } from "@angular/forms"; -import { ISitesGroup } from "../../interfaces/geom"; -import { endPoints } from "../../enum/endpoints"; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; + +import { endPoints } from '../../enum/endpoints'; +import { ISitesGroup } from '../../interfaces/geom'; +import { FormService } from '../../services/form.service'; @Component({ - selector: "monitoring-sitesgroups-create", - templateUrl: "./monitoring-sitesgroups-create.component.html", - styleUrls: ["./monitoring-sitesgroups-create.component.css"], + selector: 'monitoring-sitesgroups-create', + templateUrl: './monitoring-sitesgroups-create.component.html', + styleUrls: ['./monitoring-sitesgroups-create.component.css'], }) export class MonitoringSitesGroupsCreateComponent implements OnInit { siteGroup: ISitesGroup; form: FormGroup; + urlRelative: string; constructor( private _formService: FormService, - private _formBuilder: FormBuilder + private _formBuilder: FormBuilder, + private route: ActivatedRoute ) {} ngOnInit() { - this._formService.dataToCreate({ - module: "generic", - objectType: "sites_group", - endPoint:endPoints.sites_groups, - objSelected: {} - }); + // Remove "create" segmentUrl + this.urlRelative = '/monitorings'; + this._formService.dataToCreate( + { + module: 'generic', + objectType: 'sites_group', + id: null, + endPoint: endPoints.sites_groups, + objSelected: {}, + }, + this.urlRelative + ); this.form = this._formBuilder.group({}); } } diff --git a/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts b/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts index d7dc6a662..a940fc589 100644 --- a/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts +++ b/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts @@ -125,5 +125,4 @@ export class MonitoringSitesGroupsComponent onSelect($event) { this.geojsonService.selectSitesGroupLayer($event); } - onObjChanged($event) {} } diff --git a/frontend/app/components/monitoring-visits/monitoring-visits.component.css b/frontend/app/components/monitoring-visits/monitoring-visits.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/app/components/monitoring-visits/monitoring-visits.component.html b/frontend/app/components/monitoring-visits/monitoring-visits.component.html new file mode 100644 index 000000000..dc350d016 --- /dev/null +++ b/frontend/app/components/monitoring-visits/monitoring-visits.component.html @@ -0,0 +1,8 @@ + +
+ + + +
\ No newline at end of file diff --git a/frontend/app/components/monitoring-visits/monitoring-visits.component.ts b/frontend/app/components/monitoring-visits/monitoring-visits.component.ts new file mode 100644 index 000000000..1f4f2f27b --- /dev/null +++ b/frontend/app/components/monitoring-visits/monitoring-visits.component.ts @@ -0,0 +1,92 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { forkJoin } from 'rxjs'; +import { map, mergeMap } from 'rxjs/operators'; + +import { MonitoringGeomComponent } from '../../class/monitoring-geom-component'; +import { ISite } from '../../interfaces/geom'; +import { IPage, IPaginated } from '../../interfaces/page'; +import { IVisit } from '../../interfaces/visit'; +import { SitesService, VisitsService } from '../../services/api-geom.service'; +import { GeoJSONService } from '../../services/geojson.service'; +import { ObjectService } from '../../services/object.service'; +import { JsonData } from '../../types/jsondata'; + +@Component({ + selector: 'monitoring-visits', + templateUrl: './monitoring-visits.component.html', + styleUrls: ['./monitoring-visits.component.css'], +}) +export class MonitoringVisitsComponent extends MonitoringGeomComponent implements OnInit { + site: ISite; + @Input() visits: IVisit[]; + @Input() page: IPage; + // colsname: typeof columnNameVisit = columnNameVisit; + objectType: string; + bEdit: boolean; + objForm: FormGroup; + @Input() colsname; + objParent: any; + + constructor( + private _sites_service: SitesService, + private _visits_service: VisitsService, + private _objService: ObjectService, + public geojsonService: GeoJSONService, + private router: Router, + private _Activatedroute: ActivatedRoute, + private _formBuilder: FormBuilder + ) { + super(); + this.getAllItemsCallback = this.getVisits; + this.objectType = 'sites'; + } + + ngOnInit() { + this.objForm = this._formBuilder.group({}); + this._objService.changeObjectTypeParent(this._sites_service.objectObs, true); + this._objService.currentObjectTypeParent.subscribe((objParent) => (this.objParent = objParent)); + + this._objService.changeObjectType(this._visits_service.objectObs); + this._Activatedroute.params + .pipe( + map((params) => params['id'] as number), + mergeMap((id: number) => + forkJoin({ + site: this._sites_service.getById(id), + visits: this._visits_service.get(1, this.limit, { + id_base_site: id, + }), + }) + ) + ) + .subscribe((data: { site: ISite; visits: IPaginated }) => { + this.site = data.site; + this.setVisits(data.visits); + this.baseFilters = { id_base_site: this.site.id_base_site }; + }); + } + + getVisits(page: number, filters: JsonData) { + this._visits_service + .get(page, this.limit, filters) + .subscribe((visits: IPaginated) => this.setVisits(visits)); + } + + setVisits(visits) { + this.visits = visits.items; + this.page = { + page: visits.page - 1, + count: visits.count, + limit: visits.limit, + }; + this.colsname = this._visits_service.objectObs.dataTable.colNameObj; + } + + seeDetails($event) { + this.router.navigate([ + `monitorings/object/${$event.module.module_code}/visit/${$event.id_base_visit}`, + ]); + } +} diff --git a/frontend/app/enum/endpoints.ts b/frontend/app/enum/endpoints.ts index fc7f634af..1c278d69b 100644 --- a/frontend/app/enum/endpoints.ts +++ b/frontend/app/enum/endpoints.ts @@ -2,4 +2,5 @@ export enum endPoints { sites_groups = "sites_groups", sites = "sites", + visits = "visits", } \ No newline at end of file diff --git a/frontend/app/gnModule.module.ts b/frontend/app/gnModule.module.ts index bb57b9cbe..a3da05a3e 100644 --- a/frontend/app/gnModule.module.ts +++ b/frontend/app/gnModule.module.ts @@ -47,10 +47,13 @@ import { SitesGroupService, SitesService, ApiGeomService, + VisitsService, } from "./services/api-geom.service"; import { MonitoringSitesGroupsCreateComponent } from "./components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component"; import { MonitoringSitesCreateComponent } from "./components/monitoring-sites-create/monitoring-sites-create.component"; import { BtnSelectComponent } from "./components/btn-select/btn-select.component"; +import { MonitoringSitesEditComponent } from "./components/monitoring-sites-edit/monitoring-sites-edit.component"; +import { MonitoringVisitsComponent } from "./components/monitoring-visits/monitoring-visits.component"; // my module routing const routes: Routes = [ @@ -92,6 +95,17 @@ const routes: Routes = [ path: "create", component: MonitoringSitesCreateComponent, }, + { + path: "site/:id", + component: MonitoringVisitsComponent, + children: [ + { + path: "edit", + component: MonitoringSitesEditComponent, + }, + ] + }, + ], }, ], @@ -118,7 +132,9 @@ const routes: Routes = [ MonitoringFormComponentG, MonitoringSitesGroupsCreateComponent, MonitoringSitesCreateComponent, - BtnSelectComponent + MonitoringSitesEditComponent, + BtnSelectComponent, + MonitoringVisitsComponent ], imports: [ GN2CommonModule, @@ -151,6 +167,7 @@ const routes: Routes = [ FormService, ObjectService, ApiGeomService, + VisitsService ], bootstrap: [ModulesComponent], schemas: [ diff --git a/frontend/app/interfaces/geom.ts b/frontend/app/interfaces/geom.ts index 51ac5de25..eea5242ce 100644 --- a/frontend/app/interfaces/geom.ts +++ b/frontend/app/interfaces/geom.ts @@ -55,3 +55,9 @@ export interface ISiteType { id_nomenclature_type_site: number; label: string; } + +export interface ISiteType { + config:JsonData, + id_nomenclature_type_site:number, + label:string +} \ No newline at end of file diff --git a/frontend/app/interfaces/objObs.ts b/frontend/app/interfaces/objObs.ts index 927a65127..01f59d3e2 100644 --- a/frontend/app/interfaces/objObs.ts +++ b/frontend/app/interfaces/objObs.ts @@ -1,16 +1,17 @@ import { endPoints } from "../enum/endpoints"; import { JsonData } from "../types/jsondata"; import { ISite, ISitesGroup } from "./geom"; - -export type ObjDataType = ISite | ISitesGroup | JsonData; +import { IVisit } from "./visit"; +export type ObjDataType = ISite | ISitesGroup | IVisit | JsonData ; export interface IobjObs { properties: ObjDataType; endPoint: endPoints; - objectType: "site" | "sites_group"; + objectType: "site" | "sites_group" | "visits"; label: string; addObjLabel: string; editObjLabel: string; + addChildLabel: string; id: string | null; moduleCode: string; schema: JsonData; diff --git a/frontend/app/interfaces/object.ts b/frontend/app/interfaces/object.ts new file mode 100644 index 000000000..3e52f3935 --- /dev/null +++ b/frontend/app/interfaces/object.ts @@ -0,0 +1,20 @@ +import { JsonData } from "../types/jsondata"; +import { IPaginated } from "./page"; +import { GeoJSON } from "geojson"; +import { Observable } from "rxjs"; +import { Resp } from "../types/response"; + +export interface IObject { + data: JsonData; +} + +export interface IService { + get(limit: number, page: number, params: JsonData): Observable>; + create(postdata: T): Observable; + patch(id: number, updatedData: T): Observable; + // delete(obj: IGeomObject) +} + +export interface IGeomService extends IService { + get_geometries(params: JsonData): Observable; +} diff --git a/frontend/app/interfaces/visit.ts b/frontend/app/interfaces/visit.ts new file mode 100644 index 000000000..0404ee6e8 --- /dev/null +++ b/frontend/app/interfaces/visit.ts @@ -0,0 +1,18 @@ +import { JsonData } from "../types/jsondata"; +import { IGeomObject } from "./geom"; + +export interface IVisit extends IGeomObject { + pk:number; + comments: string; + data: JsonData; + id_base_visit: number; + id_module: number; + id_nomenclature_grp_typ: number; + id_nomenclature_tech_collect_campanule: number; + meta_create_date: Date; + meta_update_date: Date; + nb_observations: number; + uuid_base_visit: string; + visit_date_max: Date; + visit_date_min: Date; +} diff --git a/frontend/app/services/api-geom.service.ts b/frontend/app/services/api-geom.service.ts index 47fbdd4ae..92a39cf73 100644 --- a/frontend/app/services/api-geom.service.ts +++ b/frontend/app/services/api-geom.service.ts @@ -10,6 +10,7 @@ import { Resp } from '../types/response'; import { Utils } from '../utils/utils'; import { CacheService } from './cache.service'; import { ConfigJsonService } from './config-json.service'; +import { IVisit } from '../interfaces/visit'; @Injectable() export class ApiGeomService implements IGeomService { @@ -23,7 +24,7 @@ export class ApiGeomService implements IGeomService { this.init(this.endPoint, this.objectObs); } - init(endPoint, objectObjs) { + init(endPoint:endPoints, objectObjs: IobjObs) { this.endPoint = endPoint; this.objectObs = objectObjs; // this.endPoint = endPoints.sites_groups; @@ -50,8 +51,8 @@ export class ApiGeomService implements IGeomService { page: number = 1, limit: number = 10, params: JsonData = {} - ): Observable> { - return this._cacheService.request>>( + ): Observable> { + return this._cacheService.request>>( 'get', this.endPoint, { @@ -60,7 +61,7 @@ export class ApiGeomService implements IGeomService { ); } - getById(id: number): Observable { + getById(id: number): Observable { return this._cacheService.request>( 'get', `${this.endPoint}/${id}` @@ -77,7 +78,7 @@ export class ApiGeomService implements IGeomService { ); } - patch(id: number, updatedData: { properties: ISitesGroup | ISite }): Observable { + patch(id: number, updatedData: { properties: ISitesGroup | ISite | IVisit }): Observable { return this._cacheService.request('patch', `${this.endPoint}/${id}`, { postData: updatedData, }); @@ -108,6 +109,7 @@ export class SitesGroupService extends ApiGeomService { label: 'groupe de site', addObjLabel: 'Ajouter un nouveau groupe de site', editObjLabel: 'Editer le groupe de site', + addChildLabel: 'Ajouter un site', id: null, moduleCode: 'generic', schema: {}, @@ -184,6 +186,7 @@ export class SitesService extends ApiGeomService { label: 'site', addObjLabel: 'Ajouter un nouveau site', editObjLabel: 'Editer le site', + addChildLabel: 'Ajouter une visite', id: null, moduleCode: 'generic', schema: {}, @@ -256,3 +259,67 @@ export class SitesService extends ApiGeomService { return 'le site'; } } + +@Injectable() +export class VisitsService extends ApiGeomService { + constructor(_cacheService: CacheService, _configJsonService: ConfigJsonService) { + super(_cacheService, _configJsonService); + } + init(): void { + this.endPoint = endPoints.visits; + this.objectObs = { + properties: {}, + endPoint: endPoints.visits, + objectType: 'visits', + label: 'visite', + addObjLabel: 'Ajouter une nouvelle visite', + editObjLabel: 'Editer la visite', + addChildLabel: 'Ajouter une observation', + id: null, + moduleCode: 'generic', + schema: {}, + template: { + fieldNames: [], + fieldLabels: {}, + fieldNamesList: [], + fieldDefinitions: {}, + }, + dataTable: { colNameObj: {} }, + }; + this._configJsonService + .init(this.objectObs.moduleCode) + .pipe() + .subscribe(() => { + const fieldNames = this._configJsonService.configModuleObjectParam( + this.objectObs.moduleCode, + this.objectObs.objectType, + 'display_properties' + ); + const fieldNamesList = this._configJsonService.configModuleObjectParam( + this.objectObs.moduleCode, + this.objectObs.objectType, + 'display_list' + ); + const schema = this._configJsonService.schema( + this.objectObs.moduleCode, + this.objectObs.objectType + ); + const fieldLabels = this._configJsonService.fieldLabels(schema); + const fieldDefinitions = this._configJsonService.fieldDefinitions(schema); + this.objectObs.template.fieldNames = fieldNames; + this.objectObs.template.fieldNamesList = fieldNamesList; + this.objectObs.schema = schema; + this.objectObs.template.fieldLabels = fieldLabels; + this.objectObs.template.fieldDefinitions = fieldDefinitions; + this.objectObs.template.fieldNamesList = fieldNamesList; + this.objectObs.dataTable.colNameObj = Utils.toObject(fieldNamesList, fieldLabels); + }); + } + addObjectType(): string { + return " une nouvelle visite"; + } + + editObjectType(): string { + return "la visite"; + } +} \ No newline at end of file diff --git a/frontend/app/services/config-json.service.ts b/frontend/app/services/config-json.service.ts index 282910bd6..3d7e9a42f 100644 --- a/frontend/app/services/config-json.service.ts +++ b/frontend/app/services/config-json.service.ts @@ -6,6 +6,7 @@ import { of } from 'rxjs'; import { ConfigService } from './config.service'; + @Injectable() export class ConfigJsonService extends ConfigService { diff --git a/frontend/app/services/form.service.ts b/frontend/app/services/form.service.ts index 0499fccad..c8d6b0ff5 100644 --- a/frontend/app/services/form.service.ts +++ b/frontend/app/services/form.service.ts @@ -2,8 +2,8 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, forkJoin, of } from 'rxjs'; import { concatMap } from 'rxjs/operators'; -import { IobjObs, ObjDataType } from '../interfaces/objObs'; import { ISite, ISitesGroup } from '../interfaces/geom'; +import { IobjObs, ObjDataType } from '../interfaces/objObs'; import { JsonData } from '../types/jsondata'; import { Utils } from '../utils/utils'; import { MonitoringObjectService } from './monitoring-object.service'; @@ -20,22 +20,28 @@ export class FormService { constructor(private _objService: MonitoringObjectService) {} // TODO: voir si nécessaire de garder ça (objService permet d'avoir le bon objet ? et sinon modifier pour obtenir ce qu'il faut en formulaire) - changeDataSub(newDat: JsonData, objectType: string,endPoint:string,objSelected:IobjObs, moduleCode: string = 'generic') { + changeDataSub( + newDat: JsonData, + objectType: string, + endPoint: string, + moduleCode: string = 'generic' + ) { this.properties = newDat; newDat.moduleCode = moduleCode; newDat.objectType = objectType; newDat.endPoint = endPoint; - newDat.objSelect = objSelected this.dataSub.next(newDat); } - dataToCreate(newDat: JsonData, moduleCode: string = 'generic') { + dataToCreate(newDat: JsonData, urlRelative: string, moduleCode: string = 'generic') { newDat[moduleCode] = {}; newDat.moduleCode = moduleCode; + newDat.urlRelative = urlRelative; this.dataSub.next(newDat); } formValues(obj): Observable { + // const {properties ,remainaing} = obj const properties = Utils.copy(this.properties); const observables = {}; const schema = obj[obj.moduleCode]; From 8c98aacb68bd91e016d7e767245653cf82206ed9 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Tue, 18 Apr 2023 15:44:53 +0200 Subject: [PATCH 032/161] Refactor to prepare for visits (#44) * Fix/db migrations (#31) * feat(db): upgrade down_revision following rebase Since rebase with develop: changed the down_revision number * fix(db): fix bind params enabling downgrade Beforehand the downgrade was not possible... * refactor(db): removed cor_site_type_category * refactor(db): changed category into type in cor * refactor(db): create cor_type_site * fix(db): renamed column * refactor(api): update models to fit migrations * fix(db):change bib_categorie_site to bib_type_site Adding : cor_site_module cor_site_type revision alembic to create function and trigger in order to add bib_type_site but only with nomenclature 'TYPE_SITE' upgrade and downgrade works [Refs ticket]: #3 Reviewed-by: andriac * fix(api): updated models from migrations * fix(api): wip: fix admin following migrations * fix(api): update routes and tests To match migration changes * feat: flask admin bib_type_site Change bib_categories to bib_type_site into flask admin Adding filtering in list label_fr of type_site to secure the unique constraint Reviewed-by: andriac [Refs ticket]: #3 * fix(api): updated schema to match models * fix(api): module edition * style(api): uniformize type_site * style(api): change relationship name for type_site * feat(api): validator admin * fix(api): make unique BibTypeSite in admin * test(api): fix test when existing nomenclatures In database Co-authored-by: Andria Capai * refactor: object.service with observable obj Change the objectType string to objectType obj in order to subscribe to multilple properties link to this object Reviewed-by: andriacap [Refs_ticket]: #40 * feat: adapt service to get config json Get fieldsName and fieldsLabel for display properties (properties component) Add logic to setItem in LocalStorage to keepLast Value of observable on reload page TODO: - [ ] see if Localstorage is really necessary with the configService used inside api-geom.service ) - [ ] adapt monitoring-form-g.component with the new service Reviewed-by: andriacap [Refs ticket]: #4 * refactor(front): svc: better use of types * feat(front): add Visits component and service * feat(back): add get site by id route & test * feat(api): add visits routes and schema * fix(api): join modules to have the modulecode For the frontend to be able to redirect to the correct route * feat(api): add sites/id/module route To retrieve all the modules compatibles with this site This lead to add a relationship. Set to noload so that it is not loaded by other not "raiseloaded" queries * test(api): add test to test the /sites/id/module Route. Also changed some fixture to be able to write these tests * fix(front): remove double def of IGeomService * refactor(front): remove Resp interface Since it is not usefull anymore * fix(api): exclude sites relationship * fix(config): change KeyValue Since id changed * feat(front): make datatable accepts other add btn To be able to customize the "add object" button * feat(front): add btn select protocole for visit Add new component: select-btn Add call to route to get modules from a base site id Call the new component in the visit component Add interfaces Add type in config service * feat(front): parameter for label and placeholder For the btn component. Style menu and form-field to make them larger * refactor(front): api service with generic types * fix(front): div removed following rebase * refactor(front): add service as input for formComp So that formComp is more type generic * fix(config): remove "s" from sites_group * refactor(front): put initConfig in ApiService * fix(front): remove "s" from visit * fix(front): put back condition on css class To make ng-content conditionnal * fix: following rebase Fix imports, double declarations... * fix(api): redirect to url * refactor(front): rename select-btn to option-list * fix: remove contrib --------- Co-authored-by: Andria Capai --- .../config/generic/module.json | 2 +- .../gn_module_monitoring/monitoring/admin.py | 1 + .../gn_module_monitoring/monitoring/models.py | 7 +- .../monitoring/schemas.py | 2 +- backend/gn_module_monitoring/routes/site.py | 19 +- backend/gn_module_monitoring/routes/visit.py | 2 +- .../tests/fixtures/module.py | 16 + .../tests/fixtures/site.py | 16 + .../tests/fixtures/visit.py | 1 - .../tests/test_routes/test_site.py | 45 +++ .../monitoring-datatable-g.component.css | 6 + .../monitoring-datatable-g.component.html | 17 +- .../monitoring-form.component-g.ts | 12 +- .../monitoring-properties-g.component.ts | 2 +- .../monitoring-sites-create.component.html | 1 + .../monitoring-sites-create.component.ts | 2 +- .../monitoring-sites.component.html | 1 + .../monitoring-sites.component.ts | 2 +- ...nitoring-sitesgroups-create.component.html | 2 +- ...monitoring-sitesgroups-create.component.ts | 3 +- .../monitoring-visits.component.html | 6 +- .../monitoring-visits.component.ts | 27 +- .../option-list-btn.component.css | 11 + .../option-list-btn.component.html | 22 ++ .../option-list-btn.component.ts | 59 ++++ frontend/app/constants/api.ts | 1 + frontend/app/gnModule.module.ts | 4 +- frontend/app/interfaces/geom.ts | 16 +- frontend/app/interfaces/module.ts | 9 + frontend/app/interfaces/objObs.ts | 8 +- frontend/app/interfaces/object.ts | 19 +- frontend/app/interfaces/response.ts | 6 - frontend/app/interfaces/visit.ts | 4 +- frontend/app/services/api-geom.service.ts | 275 ++++++------------ frontend/app/services/config.service.ts | 2 +- frontend/app/types/response.ts | 4 - 36 files changed, 380 insertions(+), 252 deletions(-) create mode 100644 frontend/app/components/option-list-btn/option-list-btn.component.css create mode 100644 frontend/app/components/option-list-btn/option-list-btn.component.html create mode 100644 frontend/app/components/option-list-btn/option-list-btn.component.ts create mode 100644 frontend/app/constants/api.ts create mode 100644 frontend/app/interfaces/module.ts delete mode 100644 frontend/app/interfaces/response.ts delete mode 100644 frontend/app/types/response.ts diff --git a/backend/gn_module_monitoring/config/generic/module.json b/backend/gn_module_monitoring/config/generic/module.json index af77abbb1..d4444e013 100644 --- a/backend/gn_module_monitoring/config/generic/module.json +++ b/backend/gn_module_monitoring/config/generic/module.json @@ -125,7 +125,7 @@ "type_widget": "datalist", "attribut_label": "Types de sites", "type_util": "types_site", - "keyValue": "id_nomenclature", + "keyValue": "id_nomenclature_type_site", "keyLabel": "label", "multiple": true, "api" : "__MONITORINGS_PATH/sites/types", diff --git a/backend/gn_module_monitoring/monitoring/admin.py b/backend/gn_module_monitoring/monitoring/admin.py index 5e77d0a0a..aec375e21 100644 --- a/backend/gn_module_monitoring/monitoring/admin.py +++ b/backend/gn_module_monitoring/monitoring/admin.py @@ -73,3 +73,4 @@ def list_label_nomenclature_formatter(view, _context, model, _name): column_list = ("nomenclature", "config") column_formatters = dict(nomenclature=list_label_nomenclature_formatter) + form_excluded_columns = "sites" diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index a4f3493c1..0c6e2cc4c 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -104,6 +104,12 @@ class BibTypeSite(DB.Model, GenericModel): uselist=False, backref=DB.backref('bib_type_site', uselist=False) ) + + sites = DB.relationship( + "TMonitoringSites", + secondary=cor_type_site, + lazy="noload" + ) @serializable class TMonitoringObservationDetails(DB.Model): @@ -209,7 +215,6 @@ class TMonitoringVisits(TBaseVisits, GenericModel): ) ) - module = DB.relationship( TModules, lazy="select", diff --git a/backend/gn_module_monitoring/monitoring/schemas.py b/backend/gn_module_monitoring/monitoring/schemas.py index 7dde989c3..0eed83424 100644 --- a/backend/gn_module_monitoring/monitoring/schemas.py +++ b/backend/gn_module_monitoring/monitoring/schemas.py @@ -9,7 +9,7 @@ BibTypeSite, TMonitoringSites, TMonitoringSitesGroups, - TMonitoringVisits + TMonitoringVisits, ) diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py index 0e2444b37..77f123146 100644 --- a/backend/gn_module_monitoring/routes/site.py +++ b/backend/gn_module_monitoring/routes/site.py @@ -1,10 +1,13 @@ from flask import request from flask.json import jsonify +from sqlalchemy.orm import Load, joinedload from werkzeug.datastructures import MultiDict +from geonature.core.gn_commons.schemas import ModuleSchema + from gn_module_monitoring.blueprint import blueprint from gn_module_monitoring.config.repositories import get_config -from gn_module_monitoring.monitoring.models import BibTypeSite, TMonitoringSites, TNomenclatures +from gn_module_monitoring.monitoring.models import BibTypeSite, TMonitoringSites, TNomenclatures, TMonitoringModules from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema from gn_module_monitoring.routes.sites_groups import create_or_update_object_api from gn_module_monitoring.utils.routes import ( @@ -111,6 +114,20 @@ def get_all_site_geometries(): return jsonify(result) +@blueprint.route("/sites//modules", methods=["GET"]) +def get_module_by_id_base_site(id_base_site: int): + query = TMonitoringModules.query.options( + Load(TMonitoringModules).raiseload("*"), + joinedload(TMonitoringModules.types_site).options(joinedload(BibTypeSite.sites)), + ).filter(TMonitoringModules.types_site.any(BibTypeSite.sites.any(id_base_site=id_base_site))) + + result = query.all() + schema = ModuleSchema() + # TODO: Is it usefull to put a limit here? Will there be more than 200 modules? + # If limit here, implement paginated/infinite scroll on frontend side + return [schema.dump(res) for res in result] + + @blueprint.route("/sites/module/", methods=["GET"]) def get_module_sites(module_code: str): # TODO: load with site_categories.json API diff --git a/backend/gn_module_monitoring/routes/visit.py b/backend/gn_module_monitoring/routes/visit.py index 4303ba51e..c8c2e9c8c 100644 --- a/backend/gn_module_monitoring/routes/visit.py +++ b/backend/gn_module_monitoring/routes/visit.py @@ -24,7 +24,7 @@ def get_visits(): params=params, default_sort="id_base_visit", default_direction="desc" ) query = TMonitoringVisits.query.options(joinedload(TMonitoringVisits.module)) - query = filter_params(query=TMonitoringVisits.query, params=params) + query = filter_params(query=query, params=params) query = sort(query=query, sort=sort_label, sort_dir=sort_dir) return paginate( diff --git a/backend/gn_module_monitoring/tests/fixtures/module.py b/backend/gn_module_monitoring/tests/fixtures/module.py index 29f678f25..3a925cbcc 100644 --- a/backend/gn_module_monitoring/tests/fixtures/module.py +++ b/backend/gn_module_monitoring/tests/fixtures/module.py @@ -21,3 +21,19 @@ def monitoring_module(types_site): db.session.add(t_monitoring_module) 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", + ) + + with db.session.begin_nested(): + db.session.add(t_monitoring_module) + + return t_monitoring_module diff --git a/backend/gn_module_monitoring/tests/fixtures/site.py b/backend/gn_module_monitoring/tests/fixtures/site.py index 4125b3e16..4e8f289cf 100644 --- a/backend/gn_module_monitoring/tests/fixtures/site.py +++ b/backend/gn_module_monitoring/tests/fixtures/site.py @@ -23,6 +23,22 @@ def sites(users, types_site, site_group_with_sites): types_site=[types_site[key]], id_sites_group=site_group_with_sites.id_sites_group, ) + + # Add a special site that has no type + sites["no-type"] = TMonitoringSites( + id_inventor=user.id_role, + id_digitiser=user.id_role, + base_site_name=f"no-type", + base_site_description=f"Description-no-type", + base_site_code=f"Code-no-type", + geom=geom_4326, + # Random id_nomenclature_type_site + # FIXME: when id_nomenclature_type_site disapears => remove this line + id_nomenclature_type_site=list(types_site.values())[0].id_nomenclature_type_site, + types_site=[], + id_sites_group=site_group_with_sites.id_sites_group, + ) + with db.session.begin_nested(): db.session.add_all(sites.values()) return sites diff --git a/backend/gn_module_monitoring/tests/fixtures/visit.py b/backend/gn_module_monitoring/tests/fixtures/visit.py index cd7dc197d..5404952b7 100644 --- a/backend/gn_module_monitoring/tests/fixtures/visit.py +++ b/backend/gn_module_monitoring/tests/fixtures/visit.py @@ -1,7 +1,6 @@ import datetime import pytest -from geonature.tests.fixtures import datasets from geonature.utils.env import db from gn_module_monitoring.monitoring.models import TMonitoringVisits diff --git a/backend/gn_module_monitoring/tests/test_routes/test_site.py b/backend/gn_module_monitoring/tests/test_routes/test_site.py index bb00e5006..77612d0a6 100644 --- a/backend/gn_module_monitoring/tests/test_routes/test_site.py +++ b/backend/gn_module_monitoring/tests/test_routes/test_site.py @@ -57,6 +57,14 @@ def test_get_sites_id_base_site(self, sites): assert len(r.json["items"]) == 1 assert r.json["items"][0]["id_base_site"] == id_base_site + def test_get_sites_by_id(self, sites): + site = list(sites.values())[0] + id_base_site = site.id_base_site + + r = self.client.get(url_for("monitorings.get_site_by_id", id_base_site=id_base_site)) + + assert r.json["id_base_site"] == id_base_site + def test_get_all_site_geometries(self, sites): r = self.client.get(url_for("monitorings.get_all_site_geometries")) @@ -85,6 +93,43 @@ def test_get_all_site_geometries_filter_site_group(self, sites, site_group_witho features = json_resp.get("features") assert features is None + def test_get_module_by_id_base_site(self, sites, monitoring_module): + site = list(sites.values())[0] + id_base_site = site.id_base_site + + r = self.client.get( + url_for("monitorings.get_module_by_id_base_site", id_base_site=id_base_site) + ) + + expected_modules = {monitoring_module.id_module} + current_modules = {module["id_module"] for module in r.json} + assert expected_modules.issubset(current_modules) + + def test_get_module_by_id_base_site_no_type_module( + self, sites, monitoring_module_wo_types_site + ): + site = list(sites.values())[0] + id_base_site = site.id_base_site + + r = self.client.get( + url_for("monitorings.get_module_by_id_base_site", id_base_site=id_base_site) + ) + + expected_absent_modules = {monitoring_module_wo_types_site.id_module} + current_modules = {module["id_module"] for module in r.json} + assert expected_absent_modules.isdisjoint(current_modules) + + def test_get_module_by_id_base_site_no_type_site(self, sites, monitoring_module): + id_base_site = sites["no-type"].id_base_site + + r = self.client.get( + url_for("monitorings.get_module_by_id_base_site", id_base_site=id_base_site) + ) + + expected_modules = {monitoring_module.id_module} + current_modules = {module["id_module"] for module in r.json} + assert expected_modules.isdisjoint(current_modules) + def test_get_module_sites(self): module_code = "TEST" r = self.client.get(url_for("monitorings.get_module_sites", module_code=module_code)) diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css index 11176aee6..fa047afa2 100644 --- a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css @@ -45,3 +45,9 @@ .custom-dt { box-shadow: none !important; } + +/* Work around to make ng-content conditional +(not supported, maybe in Angular16) */ +.wrapper-button:not(:empty) + .default-button { + display: none; +} \ No newline at end of file diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html index 095b562a1..0a65158a5 100644 --- a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html @@ -7,7 +7,7 @@ (keyup)="updateFilter($event)" /> --> -
+
- - - +
+ +
+
+ + + +
(); @Input() sites: {}; + @Input() apiService: ApiGeomService; dataForm: IDataForm; searchSite = ''; @@ -58,7 +59,6 @@ export class MonitoringFormComponentG implements OnInit { private _commonService: CommonService, private _dynformService: DynamicFormService, private _formService: FormService, - private _apiGeomService: ApiGeomService, private _router: Router, ) {} @@ -69,7 +69,7 @@ export class MonitoringFormComponentG implements OnInit { tap((data) => { this.obj = data; this.obj.bIsInitialized = true; - this._apiGeomService.init(this.obj.endPoint, this.obj.objSelected); + this.apiService.init(this.obj.endPoint, this.obj.objSelected); }), mergeMap((data: any) => this._configService.init(data.moduleCode)) ) @@ -272,7 +272,7 @@ export class MonitoringFormComponentG implements OnInit { const urlPathDetail = [this.obj.urlRelative].concat(urlSegment).join('/'); this.objChanged.emit(this.obj); this.bEditChange.emit(false); - this._router.navigateByUrl(urlPathDetail); + this.obj.urlRelative ? this._router.navigateByUrl(urlPathDetail): null; } /** @@ -296,8 +296,8 @@ export class MonitoringFormComponentG implements OnInit { const objToUpdateOrCreate = this._formService.postData(sendValue, this.obj); console.log(objToUpdateOrCreate); const action = this.obj.id - ? this._apiGeomService.patch(this.obj.id, objToUpdateOrCreate) - : this._apiGeomService.create(objToUpdateOrCreate); + ? this.apiService.patch(this.obj.id, objToUpdateOrCreate) + : this.apiService.create(objToUpdateOrCreate); const actionLabel = this.obj.id ? 'Modification' : 'Création'; action.subscribe((objData) => { this._commonService.regularToaster('success', this.msgToaster(actionLabel)); @@ -344,7 +344,7 @@ export class MonitoringFormComponentG implements OnInit { this.bDeleteSpinner = true; this._commonService.regularToaster('info', this.msgToaster('Suppression')); // : this.obj.post(this.objForm.value); - this._apiGeomService.delete(this.obj.id).subscribe((del) => { + this.apiService.delete(this.obj.id).subscribe((del) => { this.bDeleteSpinner = this.bDeleteModal = false; this.objChanged.emit(this.obj); setTimeout(() => { diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts index 3c411bf06..ec93302e3 100644 --- a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts @@ -24,7 +24,7 @@ export class MonitoringPropertiesGComponent implements OnInit { color: string = 'white'; dataDetails: ISitesGroup; fields: JsonData; - fieldDefinitions: JsonData; + fieldDefinitions: JsonData = {}; fieldsNames: string[]; endPoint: string; datasetForm = new FormControl(); diff --git a/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.html b/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.html index cc95c530c..cd9a5bcd3 100644 --- a/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.html +++ b/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.html @@ -7,6 +7,7 @@ >
diff --git a/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.ts b/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.ts index 33741d789..ea83a7eb4 100644 --- a/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.ts +++ b/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.ts @@ -34,7 +34,7 @@ export class MonitoringSitesCreateComponent implements OnInit { constructor( private _formService: FormService, private _formBuilder: FormBuilder, - private siteService: SitesService, + public siteService: SitesService, private route: ActivatedRoute, private _objService: ObjectService ) {} diff --git a/frontend/app/components/monitoring-sites/monitoring-sites.component.html b/frontend/app/components/monitoring-sites/monitoring-sites.component.html index 7c7b3aa9f..943d69b13 100644 --- a/frontend/app/components/monitoring-sites/monitoring-sites.component.html +++ b/frontend/app/components/monitoring-sites/monitoring-sites.component.html @@ -6,6 +6,7 @@ >
- +
\ No newline at end of file diff --git a/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts index 3e6b774cc..7ff9a6791 100644 --- a/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts +++ b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts @@ -5,6 +5,7 @@ import { ActivatedRoute } from '@angular/router'; import { endPoints } from '../../enum/endpoints'; import { ISitesGroup } from '../../interfaces/geom'; import { FormService } from '../../services/form.service'; +import { SitesGroupService } from '../../services/api-geom.service'; @Component({ selector: 'monitoring-sitesgroups-create', @@ -18,7 +19,7 @@ export class MonitoringSitesGroupsCreateComponent implements OnInit { constructor( private _formService: FormService, private _formBuilder: FormBuilder, - private route: ActivatedRoute + public sitesGroupService: SitesGroupService ) {} ngOnInit() { diff --git a/frontend/app/components/monitoring-visits/monitoring-visits.component.html b/frontend/app/components/monitoring-visits/monitoring-visits.component.html index dc350d016..a451e2484 100644 --- a/frontend/app/components/monitoring-visits/monitoring-visits.component.html +++ b/frontend/app/components/monitoring-visits/monitoring-visits.component.html @@ -3,6 +3,8 @@ + (onFilter)="setFilter($event)" (onSetPage)="setPage($event)" [obj]="visits" (onDetailsRow)="seeDetails($event)"> + +
\ No newline at end of file diff --git a/frontend/app/components/monitoring-visits/monitoring-visits.component.ts b/frontend/app/components/monitoring-visits/monitoring-visits.component.ts index 1f4f2f27b..b9a98545e 100644 --- a/frontend/app/components/monitoring-visits/monitoring-visits.component.ts +++ b/frontend/app/components/monitoring-visits/monitoring-visits.component.ts @@ -12,6 +12,9 @@ import { SitesService, VisitsService } from '../../services/api-geom.service'; import { GeoJSONService } from '../../services/geojson.service'; import { ObjectService } from '../../services/object.service'; import { JsonData } from '../../types/jsondata'; +import { SelectObject } from '../../interfaces/object'; +import { Module } from '../../interfaces/module'; +import { ConfigService } from '../../services/config.service'; @Component({ selector: 'monitoring-visits', @@ -28,6 +31,7 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement objForm: FormGroup; @Input() colsname; objParent: any; + modules: SelectObject[]; constructor( private _sites_service: SitesService, @@ -36,7 +40,8 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement public geojsonService: GeoJSONService, private router: Router, private _Activatedroute: ActivatedRoute, - private _formBuilder: FormBuilder + private _formBuilder: FormBuilder, + private _configService: ConfigService ) { super(); this.getAllItemsCallback = this.getVisits; @@ -89,4 +94,24 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement `monitorings/object/${$event.module.module_code}/visit/${$event.id_base_visit}`, ]); } + + getModules() { + this._sites_service.getSiteModules(this.site.id_base_site).subscribe( + (data: Module[]) => + (this.modules = data.map((item) => { + return { id: item.module_code, label: item.module_label }; + })) + ); + } + + addNewVisit($event: SelectObject) { + const moduleCode = $event.id; + //create_object/cheveches_sites_group/visit?id_base_site=47 + this._configService.init(moduleCode).subscribe(() => { + this.router.navigate([ + `monitorings/create_object/${moduleCode}/visit`, + { queryParams: { id_base_site: this.site.id_base_site } }, + ]); + }); + } } diff --git a/frontend/app/components/option-list-btn/option-list-btn.component.css b/frontend/app/components/option-list-btn/option-list-btn.component.css new file mode 100644 index 000000000..af550605f --- /dev/null +++ b/frontend/app/components/option-list-btn/option-list-btn.component.css @@ -0,0 +1,11 @@ +.dropdown { + padding: 5px; +} + +::ng-deep.mat-menu-panel.btn-menu { + width: 400px; +} + +::ng-deep.mat-form-field.btn-menu { + display: block; +} diff --git a/frontend/app/components/option-list-btn/option-list-btn.component.html b/frontend/app/components/option-list-btn/option-list-btn.component.html new file mode 100644 index 000000000..a82dd2548 --- /dev/null +++ b/frontend/app/components/option-list-btn/option-list-btn.component.html @@ -0,0 +1,22 @@ + + + + \ No newline at end of file diff --git a/frontend/app/components/option-list-btn/option-list-btn.component.ts b/frontend/app/components/option-list-btn/option-list-btn.component.ts new file mode 100644 index 000000000..5abee42ae --- /dev/null +++ b/frontend/app/components/option-list-btn/option-list-btn.component.ts @@ -0,0 +1,59 @@ +import { + Component, + ViewChild, + Input, + Output, + EventEmitter, +} from "@angular/core"; +import { FormControl } from "@angular/forms"; +import { MatMenuTrigger } from "@angular/material/menu"; +import { SelectObject } from "../../interfaces/object"; + +@Component({ + selector: "option-list-btn", + templateUrl: "./option-list-btn.component.html", + styleUrls: ["./option-list-btn.component.css"], +}) +export class OptionListButtonComponent { + @ViewChild(MatMenuTrigger) ddTrigger: MatMenuTrigger; + + form = new FormControl(); + private _optionList: SelectObject[]; + @Input() set optionList(value: SelectObject[]) { + this._optionList = value; + } + + get optionList(): SelectObject[] { + // other logic + return this._optionList; + } + @Input() label: string = ""; + @Input() placeholder: string = ""; + @Output() onSaved = new EventEmitter(); + @Output() onDeployed = new EventEmitter(); + + constructor() {} + + cancelClick(ev: MouseEvent) { + ev.stopPropagation(); + } + + onCancel() { + this.ddTrigger.closeMenu(); + } + + onSave() { + this.ddTrigger.closeMenu(); + this.onSaved.emit(this.form.value); + } + + onDeploy() { + this.onDeployed.emit(); + } + + displayFn(value: SelectObject) { + if (value) { + return value.label; + } + } +} diff --git a/frontend/app/constants/api.ts b/frontend/app/constants/api.ts new file mode 100644 index 000000000..4a3dc15e2 --- /dev/null +++ b/frontend/app/constants/api.ts @@ -0,0 +1 @@ +export const LIMIT = 10; diff --git a/frontend/app/gnModule.module.ts b/frontend/app/gnModule.module.ts index a3da05a3e..6ef0e9801 100644 --- a/frontend/app/gnModule.module.ts +++ b/frontend/app/gnModule.module.ts @@ -54,6 +54,7 @@ import { MonitoringSitesCreateComponent } from "./components/monitoring-sites-cr import { BtnSelectComponent } from "./components/btn-select/btn-select.component"; import { MonitoringSitesEditComponent } from "./components/monitoring-sites-edit/monitoring-sites-edit.component"; import { MonitoringVisitsComponent } from "./components/monitoring-visits/monitoring-visits.component"; +import { OptionListButtonComponent } from "./components/option-list-btn/option-list-btn.component"; // my module routing const routes: Routes = [ @@ -134,7 +135,8 @@ const routes: Routes = [ MonitoringSitesCreateComponent, MonitoringSitesEditComponent, BtnSelectComponent, - MonitoringVisitsComponent + MonitoringVisitsComponent, + OptionListButtonComponent, ], imports: [ GN2CommonModule, diff --git a/frontend/app/interfaces/geom.ts b/frontend/app/interfaces/geom.ts index eea5242ce..de69a3b40 100644 --- a/frontend/app/interfaces/geom.ts +++ b/frontend/app/interfaces/geom.ts @@ -1,11 +1,9 @@ import { GeoJSON } from 'geojson'; import { Observable } from 'rxjs'; import { JsonData } from '../types/jsondata'; -import { Resp } from '../types/response'; -import { IPaginated } from './page'; +import { IObject, IService } from './object'; -export interface IGeomObject { - data: JsonData; +export interface IGeomObject extends IObject { geometry: GeoJSON.Geometry; } @@ -38,16 +36,8 @@ export interface ISite extends IGeomObject { uuid_base_site: string; } -interface IGeomObjectProperties { - properties: IGeomObject; -} - -export interface IGeomService { - get(limit: number, page: number, params: JsonData): Observable>; +export interface IGeomService extends IService { get_geometries(params: JsonData): Observable; - create(postdata: IGeomObjectProperties): Observable; - patch(id: number, updatedData: IGeomObjectProperties): Observable; - delete(id: number); } export interface ISiteType { diff --git a/frontend/app/interfaces/module.ts b/frontend/app/interfaces/module.ts new file mode 100644 index 000000000..5fe510d71 --- /dev/null +++ b/frontend/app/interfaces/module.ts @@ -0,0 +1,9 @@ +export type Module = { + id_module: number; + meta_create_date: Date; + meta_update_date: Date; + module_code: string; + module_label: string; + ng_module: string | null; + type: string; +}; diff --git a/frontend/app/interfaces/objObs.ts b/frontend/app/interfaces/objObs.ts index 01f59d3e2..311ed4e4c 100644 --- a/frontend/app/interfaces/objObs.ts +++ b/frontend/app/interfaces/objObs.ts @@ -2,12 +2,12 @@ import { endPoints } from "../enum/endpoints"; import { JsonData } from "../types/jsondata"; import { ISite, ISitesGroup } from "./geom"; import { IVisit } from "./visit"; -export type ObjDataType = ISite | ISitesGroup | IVisit | JsonData ; -export interface IobjObs { - properties: ObjDataType; +export type ObjDataType = ISite | ISitesGroup | IVisit ; +export interface IobjObs { + properties: T | {}; endPoint: endPoints; - objectType: "site" | "sites_group" | "visits"; + objectType: "site" | "sites_group" | "visit"; label: string; addObjLabel: string; editObjLabel: string; diff --git a/frontend/app/interfaces/object.ts b/frontend/app/interfaces/object.ts index 3e52f3935..cec2af00d 100644 --- a/frontend/app/interfaces/object.ts +++ b/frontend/app/interfaces/object.ts @@ -2,19 +2,24 @@ import { JsonData } from "../types/jsondata"; import { IPaginated } from "./page"; import { GeoJSON } from "geojson"; import { Observable } from "rxjs"; -import { Resp } from "../types/response"; export interface IObject { data: JsonData; } +export interface IObjectProperties { + properties: T; +} + export interface IService { get(limit: number, page: number, params: JsonData): Observable>; - create(postdata: T): Observable; - patch(id: number, updatedData: T): Observable; - // delete(obj: IGeomObject) + create(postdata: IObjectProperties): Observable; + patch(id: number, updatedData: IObjectProperties): Observable; + delete(id: number): Observable; } -export interface IGeomService extends IService { - get_geometries(params: JsonData): Observable; -} +export type SelectObject = { + id: string; + label: string; +}; + diff --git a/frontend/app/interfaces/response.ts b/frontend/app/interfaces/response.ts deleted file mode 100644 index 317f5c94d..000000000 --- a/frontend/app/interfaces/response.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { JsonData } from "../types/jsondata"; -export interface ResponseUpdated { - message: string; - payload: JsonData; - status_code: number; -} diff --git a/frontend/app/interfaces/visit.ts b/frontend/app/interfaces/visit.ts index 0404ee6e8..c285fcd13 100644 --- a/frontend/app/interfaces/visit.ts +++ b/frontend/app/interfaces/visit.ts @@ -1,7 +1,7 @@ import { JsonData } from "../types/jsondata"; -import { IGeomObject } from "./geom"; +import { IObject } from "./object"; -export interface IVisit extends IGeomObject { +export interface IVisit extends IObject { pk:number; comments: string; data: JsonData; diff --git a/frontend/app/services/api-geom.service.ts b/frontend/app/services/api-geom.service.ts index 92a39cf73..f9e942718 100644 --- a/frontend/app/services/api-geom.service.ts +++ b/frontend/app/services/api-geom.service.ts @@ -2,70 +2,94 @@ import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { endPoints } from '../enum/endpoints'; -import { IGeomService, ISite, ISiteType, ISitesGroup } from '../interfaces/geom'; -import { IobjObs, ObjDataType } from '../interfaces/objObs'; +import { IGeomObject, IGeomService, ISite, ISiteType, ISitesGroup } from '../interfaces/geom'; +import { IobjObs } from '../interfaces/objObs'; import { IPaginated } from '../interfaces/page'; import { JsonData } from '../types/jsondata'; -import { Resp } from '../types/response'; import { Utils } from '../utils/utils'; import { CacheService } from './cache.service'; import { ConfigJsonService } from './config-json.service'; import { IVisit } from '../interfaces/visit'; +import { IObject, IObjectProperties, IService } from '../interfaces/object'; +import { LIMIT } from '../constants/api'; +import { Module } from '../interfaces/module'; @Injectable() -export class ApiGeomService implements IGeomService { +export class ApiService implements IService { + public objectObs: IobjObs; public endPoint: endPoints; - public objectObs: IobjObs; - constructor( protected _cacheService: CacheService, protected _configJsonService: ConfigJsonService - ) { - this.init(this.endPoint, this.objectObs); - } + ) {} - init(endPoint:endPoints, objectObjs: IobjObs) { + init(endPoint: endPoints, objectObjs: IobjObs) { this.endPoint = endPoint; this.objectObs = objectObjs; - // this.endPoint = endPoints.sites_groups; - // this.objectObs = { - // properties: {}, - // endPoint: endPoints.sites_groups, - // objectType: 'sites_group', - // label: 'groupe de site', - // addObjLabel: 'Ajouter', - // editObjLabel: 'Editer', - // id: null, - // moduleCode: 'generic', - // schema: {}, - // template: { - // fieldNames: [], - // fieldLabels: {}, - // fieldNamesList: [], - // fieldDefinitions: {}, - // }, - // dataTable: { colNameObj: {} }, - // }; + this.initConfig(); } - get( - page: number = 1, - limit: number = 10, - params: JsonData = {} - ): Observable> { - return this._cacheService.request>>( - 'get', - this.endPoint, - { - queryParams: { page, limit, ...params }, - } - ); + + private initConfig(): void { + this._configJsonService + .init(this.objectObs.moduleCode) + .pipe() + .subscribe(() => { + const fieldNames = this._configJsonService.configModuleObjectParam( + this.objectObs.moduleCode, + this.objectObs.objectType, + 'display_properties' + ); + //FIXME: same as site group: to refact + const fieldNamesList = this._configJsonService.configModuleObjectParam( + this.objectObs.moduleCode, + this.objectObs.objectType, + 'display_list' + ); + const schema = this._configJsonService.schema( + this.objectObs.moduleCode, + this.objectObs.objectType + ); + const fieldLabels = this._configJsonService.fieldLabels(schema); + const fieldDefinitions = this._configJsonService.fieldDefinitions(schema); + this.objectObs.template.fieldNames = fieldNames; + this.objectObs.template.fieldNamesList = fieldNamesList; + this.objectObs.schema = schema; + this.objectObs.template.fieldLabels = fieldLabels; + this.objectObs.template.fieldDefinitions = fieldDefinitions; + this.objectObs.template.fieldNamesList = fieldNamesList; + this.objectObs.dataTable.colNameObj = Utils.toObject(fieldNamesList, fieldLabels); + }); + } + get(page: number = 1, limit: number = LIMIT, params: JsonData = {}): Observable> { + return this._cacheService.request>>('get', this.objectObs.endPoint, { + queryParams: { page, limit, ...params }, + }); } - getById(id: number): Observable { - return this._cacheService.request>( - 'get', - `${this.endPoint}/${id}` - ); + getById(id: number): Observable { + return this._cacheService.request>('get', `${this.objectObs.endPoint}/${id}`); + } + patch(id: number, updatedData: IObjectProperties): Observable { + return this._cacheService.request('patch', `${this.objectObs.endPoint}/${id}`, { + postData: updatedData as {}, + }); + } + + create(postData: IObjectProperties): Observable { + return this._cacheService.request('post', `${this.objectObs.endPoint}`, { + postData: postData as {}, + }); + } + + delete(id: number): Observable { + return this._cacheService.request('delete', `${this.objectObs.endPoint}/${id}`); + } +} +@Injectable() +export class ApiGeomService extends ApiService implements IGeomService { + constructor(protected _cacheService: CacheService, protected _configJsonService: ConfigJsonService) { + super(_cacheService, _configJsonService); + this.init(this.endPoint, this.objectObs); } get_geometries(params: JsonData = {}): Observable { @@ -77,32 +101,16 @@ export class ApiGeomService implements IGeomService { } ); } - - patch(id: number, updatedData: { properties: ISitesGroup | ISite | IVisit }): Observable { - return this._cacheService.request('patch', `${this.endPoint}/${id}`, { - postData: updatedData, - }); - } - - create(postData: { properties: ISitesGroup | ISite }): Observable { - return this._cacheService.request('post', `${this.endPoint}`, { - postData: postData, - }); - } - - delete(id: number): Observable { - return this._cacheService.request('delete', `${this.endPoint}/${id}`); - } } @Injectable() -export class SitesGroupService extends ApiGeomService { +export class SitesGroupService extends ApiGeomService { constructor(_cacheService: CacheService, _configJsonService: ConfigJsonService) { super(_cacheService, _configJsonService); } init(): void { - this.endPoint = endPoints.sites_groups; - this.objectObs = { + const endPoint = endPoints.sites_groups; + const objectObs: IobjObs = { properties: {}, endPoint: endPoints.sites_groups, objectType: 'sites_group', @@ -121,34 +129,7 @@ export class SitesGroupService extends ApiGeomService { }, dataTable: { colNameObj: {} }, }; - this._configJsonService - .init(this.objectObs.moduleCode) - .pipe() - .subscribe(() => { - const fieldNames = this._configJsonService.configModuleObjectParam( - this.objectObs.moduleCode, - this.objectObs.objectType, - 'display_properties' - ); - const fieldNamesList = this._configJsonService.configModuleObjectParam( - this.objectObs.moduleCode, - this.objectObs.objectType, - 'display_list' - ); - const schema = this._configJsonService.schema( - this.objectObs.moduleCode, - this.objectObs.objectType - ); - const fieldLabels = this._configJsonService.fieldLabels(schema); - const fieldDefinitions = this._configJsonService.fieldDefinitions(schema); - this.objectObs.template.fieldNames = fieldNames; - this.objectObs.template.fieldNamesList = fieldNamesList; - this.objectObs.schema = schema; - this.objectObs.template.fieldLabels = fieldLabels; - this.objectObs.template.fieldDefinitions = fieldDefinitions; - this.objectObs.template.fieldNamesList = fieldNamesList; - this.objectObs.dataTable.colNameObj = Utils.toObject(fieldNamesList, fieldLabels); - }); + super.init(endPoint, objectObs); } getSitesChild( @@ -160,26 +141,17 @@ export class SitesGroupService extends ApiGeomService { queryParams: { page, limit, ...params }, }); } - - addObjectType(): string { - return 'un nouveau groupe de site'; - } - - editObjectType(): string { - return 'le groupe de site'; - } } @Injectable() -export class SitesService extends ApiGeomService { +export class SitesService extends ApiGeomService { constructor(_cacheService: CacheService, _configJsonService: ConfigJsonService) { super(_cacheService, _configJsonService); } - opts = []; init(): void { - this.endPoint = endPoints.sites; - this.objectObs = { + const endPoint = endPoints.sites; + const objectObs: IobjObs = { properties: {}, endPoint: endPoints.sites, objectType: 'site', @@ -198,45 +170,9 @@ export class SitesService extends ApiGeomService { }, dataTable: { colNameObj: {} }, }; - this._configJsonService - .init(this.objectObs.moduleCode) - .pipe() - .subscribe(() => { - const fieldNames = this._configJsonService.configModuleObjectParam( - this.objectObs.moduleCode, - this.objectObs.objectType, - 'display_properties' - ); - //FIXME: same as site group: to refact - const fieldNamesList = this._configJsonService.configModuleObjectParam( - this.objectObs.moduleCode, - this.objectObs.objectType, - 'display_list' - ); - const schema = this._configJsonService.schema( - this.objectObs.moduleCode, - this.objectObs.objectType - ); - const fieldLabels = this._configJsonService.fieldLabels(schema); - const fieldDefinitions = this._configJsonService.fieldDefinitions(schema); - this.objectObs.template.fieldNames = fieldNames; - this.objectObs.template.fieldNamesList = fieldNamesList; - this.objectObs.schema = schema; - this.objectObs.template.fieldLabels = fieldLabels; - this.objectObs.template.fieldDefinitions = fieldDefinitions; - this.objectObs.template.fieldNamesList = fieldNamesList; - this.objectObs.dataTable.colNameObj = Utils.toObject(fieldNamesList, fieldLabels); - }); + super.init(endPoint, objectObs); } - // getTypeSites( - // ): Observable> { - // return this._cacheService.request>>( - // "get", - // "sites/types" - // ); - // } - getTypeSites( page: number = 1, limit: number = 10, @@ -251,26 +187,23 @@ export class SitesService extends ApiGeomService { ); } - addObjectType(): string { - return ' un nouveau site'; - } - - editObjectType(): string { - return 'le site'; + getSiteModules(idSite: number): Observable { + return this._cacheService.request('get', `sites/${idSite}/modules`); } } @Injectable() -export class VisitsService extends ApiGeomService { +export class VisitsService extends ApiService { constructor(_cacheService: CacheService, _configJsonService: ConfigJsonService) { super(_cacheService, _configJsonService); + this.init(); } init(): void { - this.endPoint = endPoints.visits; - this.objectObs = { + const endPoint = endPoints.visits; + const objectObs: IobjObs = { properties: {}, endPoint: endPoints.visits, - objectType: 'visits', + objectType: 'visit', label: 'visite', addObjLabel: 'Ajouter une nouvelle visite', editObjLabel: 'Editer la visite', @@ -286,40 +219,6 @@ export class VisitsService extends ApiGeomService { }, dataTable: { colNameObj: {} }, }; - this._configJsonService - .init(this.objectObs.moduleCode) - .pipe() - .subscribe(() => { - const fieldNames = this._configJsonService.configModuleObjectParam( - this.objectObs.moduleCode, - this.objectObs.objectType, - 'display_properties' - ); - const fieldNamesList = this._configJsonService.configModuleObjectParam( - this.objectObs.moduleCode, - this.objectObs.objectType, - 'display_list' - ); - const schema = this._configJsonService.schema( - this.objectObs.moduleCode, - this.objectObs.objectType - ); - const fieldLabels = this._configJsonService.fieldLabels(schema); - const fieldDefinitions = this._configJsonService.fieldDefinitions(schema); - this.objectObs.template.fieldNames = fieldNames; - this.objectObs.template.fieldNamesList = fieldNamesList; - this.objectObs.schema = schema; - this.objectObs.template.fieldLabels = fieldLabels; - this.objectObs.template.fieldDefinitions = fieldDefinitions; - this.objectObs.template.fieldNamesList = fieldNamesList; - this.objectObs.dataTable.colNameObj = Utils.toObject(fieldNamesList, fieldLabels); - }); - } - addObjectType(): string { - return " une nouvelle visite"; + super.init(endPoint, objectObs); } - - editObjectType(): string { - return "la visite"; - } -} \ No newline at end of file +} diff --git a/frontend/app/services/config.service.ts b/frontend/app/services/config.service.ts index e3470d18a..6cffbda75 100644 --- a/frontend/app/services/config.service.ts +++ b/frontend/app/services/config.service.ts @@ -13,7 +13,7 @@ export class ConfigService { /** Configuration */ - init(moduleCode: null | string = null) { + init(moduleCode: string | null = null) { // a definir ailleurs moduleCode = moduleCode || 'generic'; diff --git a/frontend/app/types/response.ts b/frontend/app/types/response.ts deleted file mode 100644 index dd26b3360..000000000 --- a/frontend/app/types/response.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { ISite, ISitesGroup } from "../interfaces/geom"; -import { ResponseUpdated } from "../interfaces/response"; - -export type Resp = ResponseUpdated | ISite | ISitesGroup; From 0819ae556dbdb39a295690de92030d89596bc510 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Tue, 18 Apr 2023 16:16:08 +0200 Subject: [PATCH 033/161] feat(front): add seeObjLabel (#45) Displayed in datatable as a link to the obj --- .../monitoring-datatable-g.component.html | 2 +- frontend/app/interfaces/objObs.ts | 1 + frontend/app/services/api-geom.service.ts | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html index 0a65158a5..8d68446db 100644 --- a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html @@ -57,7 +57,7 @@ diff --git a/frontend/app/interfaces/objObs.ts b/frontend/app/interfaces/objObs.ts index 311ed4e4c..a4a28c5de 100644 --- a/frontend/app/interfaces/objObs.ts +++ b/frontend/app/interfaces/objObs.ts @@ -11,6 +11,7 @@ export interface IobjObs { label: string; addObjLabel: string; editObjLabel: string; + seeObjLabel: string, addChildLabel: string; id: string | null; moduleCode: string; diff --git a/frontend/app/services/api-geom.service.ts b/frontend/app/services/api-geom.service.ts index f9e942718..244e72fbd 100644 --- a/frontend/app/services/api-geom.service.ts +++ b/frontend/app/services/api-geom.service.ts @@ -117,6 +117,7 @@ export class SitesGroupService extends ApiGeomService { label: 'groupe de site', addObjLabel: 'Ajouter un nouveau groupe de site', editObjLabel: 'Editer le groupe de site', + seeObjLabel: 'Consulter le groupe de site', addChildLabel: 'Ajouter un site', id: null, moduleCode: 'generic', @@ -158,6 +159,7 @@ export class SitesService extends ApiGeomService { label: 'site', addObjLabel: 'Ajouter un nouveau site', editObjLabel: 'Editer le site', + seeObjLabel: 'Consulter le site', addChildLabel: 'Ajouter une visite', id: null, moduleCode: 'generic', @@ -207,6 +209,7 @@ export class VisitsService extends ApiService { label: 'visite', addObjLabel: 'Ajouter une nouvelle visite', editObjLabel: 'Editer la visite', + seeObjLabel: 'Consulter la visite', addChildLabel: 'Ajouter une observation', id: null, moduleCode: 'generic', From 2ee2bbc9cf72a32d02214d3e1739fec430633d10 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Tue, 18 Apr 2023 16:25:28 +0200 Subject: [PATCH 034/161] Feat/get sites sites group models (#46) * fix(api): remove monitoring_g_definitions Since it prevents the module to load... * feat(api): add join relationships For sites and sites_groups * fix(config): put back id_sites_group * refactor(api): move function to prevent circular Imports! * fix(api): load sites groups from module_id * chore: remove useless code --- .../config/generic/sites_group.json | 5 ++ .../gn_module_monitoring/monitoring/base.py | 1 - .../monitoring/definitions.py | 13 +-- .../gn_module_monitoring/monitoring/models.py | 36 +++++++- .../monitoring/serializer.py | 8 +- .../gn_module_monitoring/routes/data_utils.py | 15 ++-- .../gn_module_monitoring/routes/monitoring.py | 41 +++++++++ backend/gn_module_monitoring/routes/site.py | 3 +- .../routes/sites_groups.py | 6 +- backend/gn_module_monitoring/utils/routes.py | 86 +++++++++---------- 10 files changed, 141 insertions(+), 73 deletions(-) diff --git a/backend/gn_module_monitoring/config/generic/sites_group.json b/backend/gn_module_monitoring/config/generic/sites_group.json index 0fbd0aea6..c1efb7c09 100644 --- a/backend/gn_module_monitoring/config/generic/sites_group.json +++ b/backend/gn_module_monitoring/config/generic/sites_group.json @@ -17,6 +17,11 @@ {"prop": "sites_group_code"} ], "generic": { + "id_sites_group": { + "type_widget": "text", + "attribut_label": "Id site", + "hidden": true + }, "sites_group_name": { "type_widget": "text", "attribut_label": "Nom", diff --git a/backend/gn_module_monitoring/monitoring/base.py b/backend/gn_module_monitoring/monitoring/base.py index e4a263c87..7eec33886 100644 --- a/backend/gn_module_monitoring/monitoring/base.py +++ b/backend/gn_module_monitoring/monitoring/base.py @@ -54,7 +54,6 @@ def MonitoringModel(self, object_type): monitoring_definitions = MonitoringDefinitions() -monitoring_g_definitions = MonitoringDefinitions() class MonitoringObjectBase: _object_type = None diff --git a/backend/gn_module_monitoring/monitoring/definitions.py b/backend/gn_module_monitoring/monitoring/definitions.py index 00cd791a2..8867d65bd 100644 --- a/backend/gn_module_monitoring/monitoring/definitions.py +++ b/backend/gn_module_monitoring/monitoring/definitions.py @@ -8,7 +8,7 @@ ) from .objects import MonitoringModule, MonitoringSite -from .base import monitoring_definitions, monitoring_g_definitions +from .base import monitoring_definitions from .repositories import MonitoringObject from .geom import MonitoringObjectGeom @@ -38,14 +38,3 @@ } monitoring_definitions.set(MonitoringObjects_dict, MonitoringModels_dict) - -# #####################"" -MonitoringModelsG_dict = { - x: MonitoringModels_dict[x] for x in MonitoringModels_dict if x not in "module" -} - -MonitoringObjectsG_dict = { - x: MonitoringObjects_dict[x] for x in MonitoringObjects_dict if x not in "module" -} - -monitoring_g_definitions.set(MonitoringObjectsG_dict, MonitoringModelsG_dict) diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index 0c6e2cc4c..df2a7aefd 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -1,8 +1,8 @@ """ Modèles SQLAlchemy pour les modules de suivi """ -from sqlalchemy import select, func, and_ -from sqlalchemy.orm import column_property, ColumnProperty, RelationshipProperty, class_mapper +from sqlalchemy import join, select, func, and_ +from sqlalchemy.orm import column_property, ColumnProperty, RelationshipProperty, class_mapper, aliased from sqlalchemy.dialects.postgresql import JSONB, UUID from uuid import uuid4 @@ -410,6 +410,38 @@ class TMonitoringModules(TModules): # cascade="all,delete" # ) +# Use alias since there is already a FROM caused by count (column_properties) +sites_alias = aliased(TMonitoringSites) +TMonitoringModules.sites_groups = DB.relationship( + "TMonitoringSitesGroups", + uselist=True, # pourquoi pas par defaut ? + primaryjoin=TMonitoringModules.id_module == cor_module_type.c.id_module, + secondaryjoin=and_(TMonitoringSitesGroups.id_sites_group == sites_alias.id_sites_group, sites_alias.id_base_site == cor_type_site.c.id_base_site), + secondary=join( + cor_type_site, + cor_module_type, + cor_type_site.c.id_type_site == cor_module_type.c.id_type_site, + ), + foreign_keys=[cor_type_site.c.id_base_site, cor_module_type.c.id_module], + lazy="select", + viewonly=True, +) + + +TMonitoringModules.sites = DB.relationship( + "TMonitoringSites", + uselist=True, # pourquoi pas par defaut ? + primaryjoin=TMonitoringModules.id_module == cor_module_type.c.id_module, + secondaryjoin=TMonitoringSites.id_base_site == cor_type_site.c.id_base_site, + secondary=join( + cor_type_site, + cor_module_type, + cor_type_site.c.id_type_site == cor_module_type.c.id_type_site, + ), + foreign_keys=[cor_type_site.c.id_base_site, cor_module_type.c.id_module], + lazy="select", + viewonly=True, +) TMonitoringModules.visits = DB.relationship( TMonitoringVisits, diff --git a/backend/gn_module_monitoring/monitoring/serializer.py b/backend/gn_module_monitoring/monitoring/serializer.py index c474846a0..1cd2f3374 100644 --- a/backend/gn_module_monitoring/monitoring/serializer.py +++ b/backend/gn_module_monitoring/monitoring/serializer.py @@ -4,7 +4,7 @@ import datetime import uuid from flask import current_app -from .base import MonitoringObjectBase, monitoring_definitions, monitoring_g_definitions +from .base import MonitoringObjectBase, monitoring_definitions from ..utils.utils import to_int from ..routes.data_utils import id_field_name_dict from geonature.utils.env import DB @@ -15,14 +15,13 @@ class MonitoringObjectSerializer(MonitoringObjectBase): def get_parent(self): - monitoring_def = monitoring_g_definitions if self._module_code == "generic" else monitoring_definitions parent_type = self.parent_type() if not parent_type: return if not self._parent: self._parent = ( - monitoring_def + monitoring_definitions .monitoring_object_instance( self._module_code, parent_type, @@ -66,7 +65,6 @@ def unflatten_specific_properties(self, properties): properties["data"] = data def serialize_children(self, depth): - monitoring_def = monitoring_g_definitions if self._module_code == "generic" else monitoring_definitions children_types = self.config_param('children_types') if not children_types: @@ -85,7 +83,7 @@ def serialize_children(self, depth): for child_model in getattr(self._model, relation_name): child = ( - monitoring_def + monitoring_definitions .monitoring_object_instance(self._module_code, children_type, model=child_model) ) children_of_type.append(child.serialize(depth)) diff --git a/backend/gn_module_monitoring/routes/data_utils.py b/backend/gn_module_monitoring/routes/data_utils.py index 805ea6a4f..a6bb791be 100644 --- a/backend/gn_module_monitoring/routes/data_utils.py +++ b/backend/gn_module_monitoring/routes/data_utils.py @@ -33,7 +33,13 @@ from ..blueprint import blueprint from ..config.repositories import get_config -from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups, TMonitoringSites, BibTypeSite +from gn_module_monitoring.utils.routes import get_sites_groups_from_module_id +from gn_module_monitoring.monitoring.schemas import MonitoringSitesGroupsSchema +from gn_module_monitoring.monitoring.models import ( + BibTypeSite, + TMonitoringSites, + TMonitoringSitesGroups +) model_dict = { "habitat": Habref, @@ -92,10 +98,9 @@ def get_init_data(module_code): # sites_group if "sites_group" in config: - res_sites_group = ( - DB.session.query(TMonitoringSitesGroups).filter_by(id_module=id_module).all() - ) - out["sites_group"] = [sites_group.as_dict() for sites_group in res_sites_group] + sites_groups = get_sites_groups_from_module_id(id_module) + schema = MonitoringSitesGroupsSchema() + out["sites_group"] = [schema.dump(sites_group) for sites_group in sites_groups] # dataset (cruved ??) res_dataset = ( diff --git a/backend/gn_module_monitoring/routes/monitoring.py b/backend/gn_module_monitoring/routes/monitoring.py index c39e90248..d373f167a 100644 --- a/backend/gn_module_monitoring/routes/monitoring.py +++ b/backend/gn_module_monitoring/routes/monitoring.py @@ -142,6 +142,47 @@ def create_or_update_object_api(module_code, object_type, id): ) +def create_or_update_object_api_sites_sites_group(module_code, object_type, id=None): + """ + route pour la création ou la modification d'un objet + si id est renseigné, c'est une création (PATCH) + sinon c'est une modification (POST) + + :param module_code: reference le module concerne + :param object_type: le type d'object (site, visit, obervation) + :param id : l'identifiant de l'object (de id_base_site pour site) + :type module_code: str + :type object_type: str + :type id: int + :return: renvoie l'object crée ou modifié + :rtype: dict + """ + depth = to_int(request.args.get("depth", 1)) + + # recupération des données post + post_data = dict(request.get_json()) + if module_code != "generic": + module = get_module("module_code", module_code) + else: + module = {"id_module": "generic"} + # TODO : A enlever une fois que le post_data contiendra geometry et type depuis le front + if object_type == "site": + post_data["geometry"] = {"type": "Point", "coordinates": [2.5, 50]} + post_data["type"] = "Feature" + # on rajoute id_module s'il n'est pas renseigné par défaut ?? + if "id_module" not in post_data["properties"]: + module["id_module"] = "generic" + post_data["properties"]["id_module"] = module["id_module"] + else: + post_data["properties"]["id_module"] = module.id_module + + return ( + monitoring_definitions.monitoring_object_instance(module_code, object_type, id) + .create_or_update(post_data) + .serialize(depth) + ) + + # update object @blueprint.route("object///", methods=["PATCH"]) @blueprint.route( diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py index 77f123146..e11075dc5 100644 --- a/backend/gn_module_monitoring/routes/site.py +++ b/backend/gn_module_monitoring/routes/site.py @@ -9,9 +9,8 @@ from gn_module_monitoring.config.repositories import get_config from gn_module_monitoring.monitoring.models import BibTypeSite, TMonitoringSites, TNomenclatures, TMonitoringModules from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema -from gn_module_monitoring.routes.sites_groups import create_or_update_object_api +from gn_module_monitoring.routes.monitoring import create_or_update_object_api_sites_sites_group from gn_module_monitoring.utils.routes import ( - create_or_update_object_api_sites_sites_group, filter_params, geojson_query, get_limit_page, diff --git a/backend/gn_module_monitoring/routes/sites_groups.py b/backend/gn_module_monitoring/routes/sites_groups.py index 1c4ed6832..d9b502494 100644 --- a/backend/gn_module_monitoring/routes/sites_groups.py +++ b/backend/gn_module_monitoring/routes/sites_groups.py @@ -7,12 +7,11 @@ from gn_module_monitoring.blueprint import blueprint from gn_module_monitoring.config.repositories import get_config from gn_module_monitoring.modules.repositories import get_module -from gn_module_monitoring.monitoring.definitions import monitoring_g_definitions +from gn_module_monitoring.monitoring.definitions import monitoring_definitions from gn_module_monitoring.monitoring.models import TMonitoringSites, TMonitoringSitesGroups from gn_module_monitoring.monitoring.schemas import MonitoringSitesGroupsSchema from gn_module_monitoring.utils.errors.errorHandler import InvalidUsage from gn_module_monitoring.utils.routes import ( - create_or_update_object_api_sites_sites_group, filter_params, geojson_query, get_limit_page, @@ -20,6 +19,7 @@ paginate, sort, ) +from gn_module_monitoring.routes.monitoring import create_or_update_object_api_sites_sites_group from gn_module_monitoring.utils.utils import to_int @@ -143,7 +143,7 @@ def create_or_update_object_api(module_code, object_type, id=None): post_data["properties"]["id_module"] = module["id_module"] return ( - monitoring_g_definitions.monitoring_object_instance(module_code, object_type, id) + monitoring_definitions.monitoring_object_instance(module_code, object_type, id) .create_or_update(post_data) .serialize(depth) ) diff --git a/backend/gn_module_monitoring/utils/routes.py b/backend/gn_module_monitoring/utils/routes.py index cbd0e0b95..ab7265aea 100644 --- a/backend/gn_module_monitoring/utils/routes.py +++ b/backend/gn_module_monitoring/utils/routes.py @@ -1,19 +1,25 @@ from typing import Tuple -from flask import Response, request +from flask import Response from flask.json import jsonify from geonature.utils.env import DB -from gn_module_monitoring.modules.repositories import get_module -from gn_module_monitoring.utils.utils import to_int +from gn_module_monitoring.monitoring.models import ( + BibTypeSite, + TMonitoringSites, + TMonitoringSitesGroups, + cor_type_site, + TBaseSites, + cor_module_type, + TModules, +) from marshmallow import Schema from sqlalchemy import cast, func, text from sqlalchemy.dialects.postgresql import JSON -from sqlalchemy.orm import Query +from sqlalchemy.orm import Query, load_only from werkzeug.datastructures import MultiDict from gn_module_monitoring.monitoring.queries import Query as MonitoringQuery from gn_module_monitoring.monitoring.schemas import paginate_schema -from gn_module_monitoring.monitoring.definitions import monitoring_g_definitions def get_limit_page(params: MultiDict) -> Tuple[int]: @@ -62,42 +68,36 @@ def geojson_query(subquery) -> bytes: return b"" -def create_or_update_object_api_sites_sites_group(module_code, object_type, id=None): - """ - route pour la création ou la modification d'un objet - si id est renseigné, c'est une création (PATCH) - sinon c'est une modification (POST) - - :param module_code: reference le module concerne - :param object_type: le type d'object (site, visit, obervation) - :param id : l'identifiant de l'object (de id_base_site pour site) - :type module_code: str - :type object_type: str - :type id: int - :return: renvoie l'object crée ou modifié - :rtype: dict - """ - depth = to_int(request.args.get("depth", 1)) - - # recupération des données post - post_data = dict(request.get_json()) - if module_code != "generic": - module = get_module("module_code", module_code) - else: - module = {"id_module": "generic"} - # TODO : A enlever une fois que le post_data contiendra geometry et type depuis le front - if object_type == "site": - post_data["geometry"] = {"type": "Point", "coordinates": [2.5, 50]} - post_data["type"] = "Feature" - # on rajoute id_module s'il n'est pas renseigné par défaut ?? - if "id_module" not in post_data["properties"]: - module["id_module"] = "generic" - post_data["properties"]["id_module"] = module["id_module"] - else: - post_data["properties"]["id_module"] = module.id_module - - return ( - monitoring_g_definitions.monitoring_object_instance(module_code, object_type, id) - .create_or_update(post_data) - .serialize(depth) +def get_sites_groups_from_module_id(module_id: int): + # query = TMonitoringSitesGroups.query.options( + # # Load(TMonitoringSitesGroups).raiseload("*"), + # load_only(TMonitoringSitesGroups.id_sites_group), + # joinedload(TMonitoringSitesGroups.sites).options( + # joinedload(TMonitoringSites.types_site).options(joinedload(BibTypeSite.modules)) + # ), + # ).filter(TMonitoringModules.id_module == module_id) + + query = ( + TMonitoringSitesGroups.query.options( + # Load(TMonitoringSitesGroups).raiseload("*"), + load_only(TMonitoringSitesGroups.id_sites_group) + ) + .join( + TMonitoringSites, + TMonitoringSites.id_sites_group == TMonitoringSitesGroups.id_sites_group, + ) + .join(cor_type_site, cor_type_site.c.id_base_site == TBaseSites.id_base_site) + .join( + BibTypeSite, + BibTypeSite.id_nomenclature_type_site == cor_type_site.c.id_type_site, + ) + .join( + cor_module_type, + cor_module_type.c.id_type_site == BibTypeSite.id_nomenclature_type_site, + ) + .join(TModules, TModules.id_module == cor_module_type.c.id_module) + .filter(TModules.id_module == module_id) ) + + return query.all() + From ee1358eb10e1f319d6cfba28e75970f79191b39a Mon Sep 17 00:00:00 2001 From: andriacap <111564663+andriacap@users.noreply.github.com> Date: Tue, 25 Apr 2023 16:38:26 +0200 Subject: [PATCH 035/161] test: routes for type site and post site (#57) * test: routes for type site and post site Reviewed-by: andriac * test: apply black and change query string - Keep site beauce it's specific one cannot re use existing - Fix mnemonique "None" with existing label_default Reviewed-by:andriac --- .../fixtures/TestData/config_type_site.json | 106 ++++++++++++++++++ .../tests/fixtures/site.py | 47 ++++++++ .../tests/fixtures/type_site.py | 17 ++- .../tests/test_routes/test_site.py | 40 +++++++ 4 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 backend/gn_module_monitoring/tests/fixtures/TestData/config_type_site.json diff --git a/backend/gn_module_monitoring/tests/fixtures/TestData/config_type_site.json b/backend/gn_module_monitoring/tests/fixtures/TestData/config_type_site.json new file mode 100644 index 000000000..05b07e4c5 --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/TestData/config_type_site.json @@ -0,0 +1,106 @@ +{ + "cruved": { + "C": 1, + "U": 1, + "D": 3 + }, + "sorts": [ + { + "prop": "base_site_name", + "dir": "asc" + } + ], + "filters": {}, + "label": "Gite", + "label_list": "Gites", + "genre": "M", + "geometry_type": "Point", + "display_properties": [ + "base_site_name", + "base_site_code", + "roost_type", + "nb_visits", + "threat", + "recommandation", + "opening", + "owner_name", + "owner_adress", + "owner_tel", + "owner_mail", + "medias" + ], + "display_list": [ + "base_site_name", + "roost_type", + "nb_visits", + "owner_name" + ], + "specific": { + "roost_type": { + "type_widget": "select", + "required": true, + "attribut_label": "Type de gite", + "values": [ + "barrage", + "bâtiment", + "cave", + "Ebouli", + "église", + "façade", + "four", + "garage", + "Gîte artificiel", + "grange", + "grenier", + "grotte", + "maison", + "mine", + "mur", + "panneau", + "plancher", + "pont", + "ruine", + "toit", + "toit et volet", + "transformateur", + "tunnel", + "volet", + "Autre" + ] + }, + "place_name": { + "type_widget": "text", + "attribut_label": "Lieux-dit" + }, + "owner_name": { + "type_widget": "text", + "attribut_label": "Nom propriétaire" + }, + "owner_adress": { + "type_widget": "text", + "attribut_label": "Adresse propriétaire" + }, + "owner_tel": { + "type_widget": "text", + "attribut_label": "Tel propriétaire" + }, + "owner_mail": { + "type_widget": "text", + "attribut_label": "Email propriétaire" + }, + "opening": { + "type_widget": "textarea", + "attribut_label": "Ouverture" + }, + "threat": { + "type_widget": "textarea", + "attribut_label": "Menace(s)", + "rows": 3 + }, + "recommandation": { + "type_widget": "textarea", + "attribut_label": "Mesure(s) préconisé(s)", + "rows": 3 + } + } + } \ No newline at end of file diff --git a/backend/gn_module_monitoring/tests/fixtures/site.py b/backend/gn_module_monitoring/tests/fixtures/site.py index 4e8f289cf..8c9f6ad1c 100644 --- a/backend/gn_module_monitoring/tests/fixtures/site.py +++ b/backend/gn_module_monitoring/tests/fixtures/site.py @@ -4,6 +4,7 @@ from shapely.geometry import Point from gn_module_monitoring.monitoring.models import TMonitoringSites +from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema @pytest.fixture() @@ -42,3 +43,49 @@ def sites(users, types_site, site_group_with_sites): with db.session.begin_nested(): db.session.add_all(sites.values()) return sites + + +@pytest.fixture() +def site_to_post_with_types(users, types_site, site_group_without_sites): + user = users["user"] + geom_4326 = from_shape(Point(43, 24), srid=4326) + list_nomenclature_id = [] + specific_dic = {"owner_name": "Propriétaire", "threat": "Menaces", "owner_tel": "0609090909"} + schema_type_site = BibTypeSiteSchema() + mock_db_type_site = [schema_type_site.dump(type) for type in types_site.values()] + + for type in mock_db_type_site: + list_nomenclature_id.append(type["id_nomenclature_type_site"]) + + site_to_post_with_types = TMonitoringSites( + id_inventor=user.id_role, + id_digitiser=user.id_role, + base_site_name=f"New Site", + base_site_description=f"New Description", + base_site_code=f"New Code", + geom=geom_4326, + id_nomenclature_type_site=list_nomenclature_id[0], + types_site=list_nomenclature_id, + id_sites_group=site_group_without_sites.id_sites_group, + ) + + post_data = dict() + post_data["dataComplement"] = {} + for type_site_dic in mock_db_type_site: + copy_dic = type_site_dic.copy() + copy_dic.pop("label") + post_data["dataComplement"][type_site_dic["label"]] = copy_dic + + post_data["dataComplement"]["types_site"] = list_nomenclature_id + post_data["properties"] = MonitoringSitesSchema().dump(site_to_post_with_types) + post_data["properties"]["types_site"] = list_nomenclature_id + + for type_site in mock_db_type_site: + specific_config = type_site["config"]["specific"] + for key_specific in specific_config: + if key_specific in specific_dic.keys(): + post_data["properties"][key_specific] = specific_dic[key_specific] + else: + post_data["properties"][key_specific] = None + + return post_data diff --git a/backend/gn_module_monitoring/tests/fixtures/type_site.py b/backend/gn_module_monitoring/tests/fixtures/type_site.py index 1a213a037..9aa191673 100644 --- a/backend/gn_module_monitoring/tests/fixtures/type_site.py +++ b/backend/gn_module_monitoring/tests/fixtures/type_site.py @@ -1,3 +1,6 @@ +import json +import os + import pytest from geonature.utils.env import db from pypnnomenclature.models import BibNomenclaturesTypes, TNomenclatures @@ -5,6 +8,15 @@ from gn_module_monitoring.monitoring.models import BibTypeSite +def get_test_data(filename): + folder_path = os.path.abspath(os.path.dirname(__file__)) + folder = os.path.join(folder_path, "TestData") + jsonfile = os.path.join(folder, filename) + with open(jsonfile) as file: + data = json.load(file) + return data + + @pytest.fixture def nomenclature_types_site(): mnemoniques = ("Test_Grotte", "Test_Mine") @@ -29,9 +41,10 @@ def nomenclature_types_site(): @pytest.fixture def types_site(nomenclature_types_site): + config_type_site = get_test_data("config_type_site.json") types_site = { - nomenc_type_site.mnemonique: BibTypeSite( - id_nomenclature_type_site=nomenc_type_site.id_nomenclature, config={} + nomenc_type_site.label_default: BibTypeSite( + id_nomenclature_type_site=nomenc_type_site.id_nomenclature, config=config_type_site ) for nomenc_type_site in nomenclature_types_site } diff --git a/backend/gn_module_monitoring/tests/test_routes/test_site.py b/backend/gn_module_monitoring/tests/test_routes/test_site.py index 77612d0a6..8b7d5c9af 100644 --- a/backend/gn_module_monitoring/tests/test_routes/test_site.py +++ b/backend/gn_module_monitoring/tests/test_routes/test_site.py @@ -2,6 +2,7 @@ from flask import url_for from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema +from gn_module_monitoring.monitoring.models import TMonitoringSites @pytest.mark.usefixtures("client_class", "temporary_transaction") @@ -135,3 +136,42 @@ def test_get_module_sites(self): r = self.client.get(url_for("monitorings.get_module_sites", module_code=module_code)) assert r.json["module_code"] == module_code + + def test_get_types_site_by_label(self, types_site): + schema = BibTypeSiteSchema() + mock_db_type_site = [schema.dump(type) for type in types_site.values()] + string_contains = "e" + string_missing = "a" + + query_string = { + "limit": 100, + "page": 1, + "sort_label": "label_fr", + "sort_dir": "asc", + "label_fr": string_contains + } + r = self.client.get( + url_for("monitorings.get_types_site_by_label"), query_string=query_string + ) + assert all([string_contains in item["label"] for item in r.json["items"]]) + assert all([type in r.json["items"] for type in mock_db_type_site]) + + query_string["label_fr"] = string_missing + r = self.client.get( + url_for("monitorings.get_types_site_by_label"), query_string=query_string + ) + assert all([type not in r.json["items"] for type in mock_db_type_site]) + + def test_post_sites(self, site_to_post_with_types, types_site, site_group_without_sites): + + response = self.client.post( + url_for("monitorings.post_sites"), data=site_to_post_with_types + ) + assert response.status_code == 201 + + obj_created = response.json + res = TMonitoringSites.find_by_id(obj_created["id"]) + assert ( + res.as_dict()["base_site_name"] + == site_to_post_with_types["properties"]["base_site_name"] + ) From 6de87e1c06d762707ecb56111cc9b761085626a5 Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Tue, 25 Apr 2023 16:56:15 +0200 Subject: [PATCH 036/161] style(front): no data support & line return (#58) When no data is available: show a message When the displayed option is too long: line break --- .../option-list-btn/option-list-btn.component.css | 10 ++++++++++ .../option-list-btn/option-list-btn.component.html | 11 +++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/frontend/app/components/option-list-btn/option-list-btn.component.css b/frontend/app/components/option-list-btn/option-list-btn.component.css index af550605f..1719de53b 100644 --- a/frontend/app/components/option-list-btn/option-list-btn.component.css +++ b/frontend/app/components/option-list-btn/option-list-btn.component.css @@ -9,3 +9,13 @@ ::ng-deep.mat-form-field.btn-menu { display: block; } + + +.mat-option{ + word-wrap:break-word !important; + white-space:normal !important; +} + +::ng-deep.mat-option-text{ + line-height:normal !important; +} \ No newline at end of file diff --git a/frontend/app/components/option-list-btn/option-list-btn.component.html b/frontend/app/components/option-list-btn/option-list-btn.component.html index a82dd2548..2c6446578 100644 --- a/frontend/app/components/option-list-btn/option-list-btn.component.html +++ b/frontend/app/components/option-list-btn/option-list-btn.component.html @@ -6,10 +6,13 @@ - - - {{ option.label }} - + + Aucun protocole n'est disponible pour ce type de site + + + {{ option.label }} + +
From be4f922c280a726f86f3dda87d35c40fa2ab72ae Mon Sep 17 00:00:00 2001 From: Maxime Vergez <85738261+mvergez@users.noreply.github.com> Date: Tue, 25 Apr 2023 16:56:32 +0200 Subject: [PATCH 037/161] fix: make breadcrumb work from sites and modules (#61) --- .../gn_module_monitoring/monitoring/repositories.py | 10 ++++++---- .../monitoring-visits/monitoring-visits.component.ts | 9 +++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/backend/gn_module_monitoring/monitoring/repositories.py b/backend/gn_module_monitoring/monitoring/repositories.py index fa536efa2..8236daa20 100644 --- a/backend/gn_module_monitoring/monitoring/repositories.py +++ b/backend/gn_module_monitoring/monitoring/repositories.py @@ -165,10 +165,12 @@ def breadcrumbs(self, params): if params["parents_path"]: object_type = params.get("parents_path", []).pop() next = MonitoringObject(self._module_code, object_type) - - id_field_name = next.config_param("id_field_name") - next._id = self.get_value(id_field_name) or params.get(id_field_name) - next.get(0) + if next._object_type == "module": + next.get(field_name="module_code", value=self._module_code) + else: + id_field_name = next.config_param('id_field_name') + next._id = self.get_value(id_field_name) or params.get(id_field_name) + next.get(0) else: next = self.get_parent() diff --git a/frontend/app/components/monitoring-visits/monitoring-visits.component.ts b/frontend/app/components/monitoring-visits/monitoring-visits.component.ts index b9a98545e..3cbc34118 100644 --- a/frontend/app/components/monitoring-visits/monitoring-visits.component.ts +++ b/frontend/app/components/monitoring-visits/monitoring-visits.component.ts @@ -108,10 +108,11 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement const moduleCode = $event.id; //create_object/cheveches_sites_group/visit?id_base_site=47 this._configService.init(moduleCode).subscribe(() => { - this.router.navigate([ - `monitorings/create_object/${moduleCode}/visit`, - { queryParams: { id_base_site: this.site.id_base_site } }, - ]); + const keys = Object.keys(this._configService.config()[moduleCode]) + const parent_paths = ["sites_group", "site"].filter(item => keys.includes(item)) + this.router.navigate([`monitorings/create_object/${moduleCode}/visit`], { + queryParams: { id_base_site: this.site.id_base_site, parents_path: parent_paths }, + }); }); } } From 7375ab0f386d8145b2097cb1e412d62a6130d8a1 Mon Sep 17 00:00:00 2001 From: andriacap <111564663+andriacap@users.noreply.github.com> Date: Tue, 25 Apr 2023 16:57:05 +0200 Subject: [PATCH 038/161] feat: delete site (#62) * feat: delete site Add code from backend to delete site (frontend is already implemented) Reviewed-by: andriac [Refs_ticket]: #5 , #6 * test: add test for delete item site Reviewed-by: andriac [Refs_ticket]: #5 , #6 --- backend/gn_module_monitoring/routes/site.py | 23 ++++++++++++++++--- .../tests/test_routes/test_site.py | 15 ++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py index e11075dc5..7e25ba0b9 100644 --- a/backend/gn_module_monitoring/routes/site.py +++ b/backend/gn_module_monitoring/routes/site.py @@ -1,13 +1,18 @@ from flask import request from flask.json import jsonify +from geonature.core.gn_commons.schemas import ModuleSchema +from geonature.utils.env import db from sqlalchemy.orm import Load, joinedload from werkzeug.datastructures import MultiDict -from geonature.core.gn_commons.schemas import ModuleSchema - from gn_module_monitoring.blueprint import blueprint from gn_module_monitoring.config.repositories import get_config -from gn_module_monitoring.monitoring.models import BibTypeSite, TMonitoringSites, TNomenclatures, TMonitoringModules +from gn_module_monitoring.monitoring.models import ( + BibTypeSite, + TMonitoringModules, + TMonitoringSites, + TNomenclatures, +) from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema from gn_module_monitoring.routes.monitoring import create_or_update_object_api_sites_sites_group from gn_module_monitoring.utils.routes import ( @@ -88,12 +93,14 @@ def get_sites(): page=page, ) + @blueprint.route("/sites/", methods=["GET"]) def get_site_by_id(id_base_site): site = TMonitoringSites.query.get_or_404(id_base_site) schema = MonitoringSitesSchema() return schema.dump(site) + @blueprint.route("/sites/geometries", methods=["GET"]) def get_all_site_geometries(): params = MultiDict(request.args) @@ -144,3 +151,13 @@ def post_sites(): customConfig.update(post_data["dataComplement"][keys]["config"]) get_config(module_code, force=True, customSpecConfig=customConfig) return create_or_update_object_api_sites_sites_group(module_code, object_type), 201 + + +@blueprint.route("/sites/", methods=["DELETE"]) +def delete_site(_id): + item = TMonitoringSites.find_by_id(_id) + db.session.delete(item) + db.session.commit() + return { + "success": f"Item with {item.id_g} from table {item.__tablename__} is successfully deleted" + }, 200 diff --git a/backend/gn_module_monitoring/tests/test_routes/test_site.py b/backend/gn_module_monitoring/tests/test_routes/test_site.py index 8b7d5c9af..7b66c59a2 100644 --- a/backend/gn_module_monitoring/tests/test_routes/test_site.py +++ b/backend/gn_module_monitoring/tests/test_routes/test_site.py @@ -1,6 +1,7 @@ import pytest from flask import url_for +from gn_module_monitoring.monitoring.models import TMonitoringSites from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema from gn_module_monitoring.monitoring.models import TMonitoringSites @@ -175,3 +176,17 @@ def test_post_sites(self, site_to_post_with_types, types_site, site_group_withou res.as_dict()["base_site_name"] == site_to_post_with_types["properties"]["base_site_name"] ) + + def test_delete_site(self, sites): + site = list(sites.values())[0] + id_base_site = site.id_base_site + item = TMonitoringSites.find_by_id(id_base_site) + r = self.client.delete(url_for("monitorings.delete_site", _id=id_base_site)) + + assert ( + r.json["success"] + == f"Item with {item.id_g} from table {item.__tablename__} is successfully deleted" + ) + with pytest.raises(Exception) as e: + TMonitoringSites.query.get_or_404(id_base_site) + assert "404 Not Found" in str(e.value) From 62530183b64f0c771f3a0de6c6f83850e506ab80 Mon Sep 17 00:00:00 2001 From: andriacap <111564663+andriacap@users.noreply.github.com> Date: Wed, 26 Apr 2023 10:20:31 +0200 Subject: [PATCH 039/161] Feat/edit site (#56) * feat: edit site (init value btn-select) - Change button select to accept incoming intial values - Change visit component to add btn-select and form-g inside html - WIP: update form component when initial values is up to date Reviewed-by: andriac [Refs_ticket] : #5 , #6 * feat: get information when edit site - Get all fields from specific site.json into editform - Fix problem redirection if edit object site - WIP : check how to update specific fields from object - WIP : check how to manage listOption types site when reload or when come back into component after first init Reviewed-by: andriac [Refs_ticket]: #5 , #6 * fix: forgot add apiService property binding Reviewed-by:andriac * feat: update site with good properties Change types_site object to array of ids Remove extra key "dataComplement" to assign config Reviewed-by:andriac [Refs_ticket]: #5 , #6 * feat: prevent form appear if no type-site - Hide form if btn-list type-site not selected - Add custom error message mat-error if not selected (directive + custom message) WIP: error message is showing up only if not selected after touched . Maybe need to use asyncValidator ? Reviewed-by: andriac [Refs_tickets]: #5 , #6 , #54 * feat: change order to emit event bEdit - Change order between change formService.changeData and bEditChange - Preprocess for types_site seems to be useless (see todo) Reviewed-by: andriac [Refs_ticket]: : #5 , #6 , #54 * feat: solve request changes - Apply prettier - Change object key [''] by type properties - Remove unused preprocess_data lines [Refs_ticket] : closes #54 --- .../monitoring/objects.py | 15 +- .../monitoring/schemas.py | 27 ++-- backend/gn_module_monitoring/routes/site.py | 12 ++ .../routes/sites_groups.py | 44 ------ .../btn-select/btn-select.component.html | 3 +- .../btn-select/btn-select.component.ts | 35 ++++- .../monitoring-form.component-g.html | 2 +- .../monitoring-form.component-g.ts | 38 +++++- .../monitoring-properties-g.component.ts | 2 +- .../monitoring-sites-create.component.html | 1 + .../monitoring-visits.component.html | 50 +++++-- .../monitoring-visits.component.ts | 128 ++++++++++++++---- frontend/app/gnModule.module.ts | 3 +- frontend/app/interfaces/geom.ts | 3 + frontend/app/interfaces/object.ts | 11 +- frontend/app/services/form.service.ts | 14 +- .../app/utils/matErrorMessages.directive.ts | 37 +++++ 17 files changed, 306 insertions(+), 119 deletions(-) create mode 100644 frontend/app/utils/matErrorMessages.directive.ts diff --git a/backend/gn_module_monitoring/monitoring/objects.py b/backend/gn_module_monitoring/monitoring/objects.py index 4b49f4932..002f60113 100644 --- a/backend/gn_module_monitoring/monitoring/objects.py +++ b/backend/gn_module_monitoring/monitoring/objects.py @@ -3,7 +3,6 @@ from geonature.utils.env import DB from geonature.core.gn_commons.models import TModules - class MonitoringModule(MonitoringObject): def get(self, param_value=None, param_name=None, depth=0): """ @@ -26,12 +25,8 @@ class MonitoringSite(MonitoringObjectGeom): """ def preprocess_data(self, data): - type_site_ids = [type_site.id_nomenclature_type_site for type_site in self._model.types_site] - if len(data['types_site']) >0 : - for id_type_site in data['types_site']: - if int(id_type_site) not in type_site_ids: - type_site_ids.append(id_type_site) - #TODO: A enlever une fois qu'on aura enelever le champ "id_nomenclature_type_site" du model et de la bdd - data["id_nomenclature_type_site"]=data["types_site"][0] - - data['types_site'] = type_site_ids + if len(data["types_site"]) > 0 and all(isinstance(x, int) for x in data["types_site"]): + data["id_nomenclature_type_site"] = data["types_site"][0] + else: + data["id_nomenclature_type_site"] = data["types_site"][0]["id_nomenclature_type_site"] + # TODO: A enlever une fois qu'on aura enelever le champ "id_nomenclature_type_site" du model et de la bdd diff --git a/backend/gn_module_monitoring/monitoring/schemas.py b/backend/gn_module_monitoring/monitoring/schemas.py index 0eed83424..be2abd1b6 100644 --- a/backend/gn_module_monitoring/monitoring/schemas.py +++ b/backend/gn_module_monitoring/monitoring/schemas.py @@ -45,6 +45,19 @@ def serialize_geojson(self, obj): return json.loads(obj.geom_geojson) +class BibTypeSiteSchema(MA.SQLAlchemyAutoSchema): + label = fields.Method("get_label_from_type_site") + # See if useful in the future: + # type_site = fields.Nested(NomenclatureSchema(only=("label_fr",)), dump_only=True) + + def get_label_from_type_site(self, obj): + return obj.nomenclature.label_fr + + class Meta: + model = BibTypeSite + include_fk = True + load_instance = True + class MonitoringSitesSchema(MA.SQLAlchemyAutoSchema): class Meta: @@ -53,6 +66,7 @@ class Meta: geometry = fields.Method("serialize_geojson", dump_only=True) pk = fields.Method("set_pk",dump_only=True) + types_site = MA.Nested(BibTypeSiteSchema, many=True) def serialize_geojson(self, obj): if obj.geom is not None: @@ -61,19 +75,6 @@ def serialize_geojson(self, obj): def set_pk(self,obj): return self.Meta.model.get_id() -class BibTypeSiteSchema(MA.SQLAlchemyAutoSchema): - label = fields.Method("get_label_from_type_site") - # See if useful in the future: - # type_site = fields.Nested(NomenclatureSchema(only=("label_fr",)), dump_only=True) - - def get_label_from_type_site(self, obj): - return obj.nomenclature.label_fr - - class Meta: - model = BibTypeSite - include_fk = True - load_instance = True - class MonitoringVisitsSchema(MA.SQLAlchemyAutoSchema): class Meta: model = TMonitoringVisits diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py index 7e25ba0b9..0fd3f7d40 100644 --- a/backend/gn_module_monitoring/routes/site.py +++ b/backend/gn_module_monitoring/routes/site.py @@ -161,3 +161,15 @@ def delete_site(_id): return { "success": f"Item with {item.id_g} from table {item.__tablename__} is successfully deleted" }, 200 + +@blueprint.route("/sites/", methods=["PATCH"]) +def patch_sites(_id): + module_code = "generic" + object_type = "site" + customConfig = dict() + post_data = dict(request.get_json()) + for keys in post_data["dataComplement"].keys(): + if "config" in post_data["dataComplement"][keys]: + customConfig.update(post_data["dataComplement"][keys]["config"]) + get_config(module_code, force=True, customSpecConfig=customConfig) + return create_or_update_object_api_sites_sites_group(module_code, object_type, _id), 201 diff --git a/backend/gn_module_monitoring/routes/sites_groups.py b/backend/gn_module_monitoring/routes/sites_groups.py index d9b502494..6535b8e9a 100644 --- a/backend/gn_module_monitoring/routes/sites_groups.py +++ b/backend/gn_module_monitoring/routes/sites_groups.py @@ -103,47 +103,3 @@ def handle_validation_error(error): status_code=422, payload=error.data, ).to_dict() - - -# TODO: OPTIMIZE in order to adapt to new monitoring module (entry by sites_groups) - - -def create_or_update_object_api(module_code, object_type, id=None): - """ - route pour la création ou la modification d'un objet - si id est renseigné, c'est une création (PATCH) - sinon c'est une modification (POST) - - :param module_code: reference le module concerne - :param object_type: le type d'object (site, visit, obervation) - :param id : l'identifiant de l'object (de id_base_site pour site) - :type module_code: str - :type object_type: str - :type id: int - :return: renvoie l'object crée ou modifié - :rtype: dict - """ - depth = to_int(request.args.get("depth", 1)) - - # recupération des données post - post_data = dict(request.get_json()) - if module_code != "generic": - module = get_module("module_code", module_code) - else: - module = {"id_module": "generic"} - #TODO : A enlever une fois que le post_data contiendra geometry et type depuis le front - if object_type == "site": - post_data["geometry"]={'type':'Point', 'coordinates':[2.5,50]} - post_data["type"]='Feature' - # on rajoute id_module s'il n'est pas renseigné par défaut ?? - if "id_module" not in post_data["properties"]: - module["id_module"] = "generic" - post_data["properties"]["id_module"] = module["id_module"] - else: - post_data["properties"]["id_module"] = module["id_module"] - - return ( - monitoring_definitions.monitoring_object_instance(module_code, object_type, id) - .create_or_update(post_data) - .serialize(depth) - ) diff --git a/frontend/app/components/btn-select/btn-select.component.html b/frontend/app/components/btn-select/btn-select.component.html index c852dfbd1..3a47c7e6a 100644 --- a/frontend/app/components/btn-select/btn-select.component.html +++ b/frontend/app/components/btn-select/btn-select.component.html @@ -1,6 +1,6 @@ {{ titleBtn }} - + + diff --git a/frontend/app/components/btn-select/btn-select.component.ts b/frontend/app/components/btn-select/btn-select.component.ts index 345a4ab6e..e6df25e6a 100644 --- a/frontend/app/components/btn-select/btn-select.component.ts +++ b/frontend/app/components/btn-select/btn-select.component.ts @@ -8,12 +8,13 @@ import { Output, ViewChild, } from '@angular/core'; -import { FormControl } from '@angular/forms'; +import { FormControl, Validators } from '@angular/forms'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { Observable, iif, of } from 'rxjs'; import { debounceTime, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'; import { JsonData } from '../../types/jsondata'; +import { FormService } from '../../services/form.service'; export interface EmptyObject { name: string; @@ -27,8 +28,10 @@ export interface EmptyObject { export class BtnSelectComponent implements OnInit { selectable = true; removable = true; + isInit = false; separatorKeysCodes: number[] = [ENTER, COMMA]; myControl = new FormControl(); + listOpNeeded = new FormControl([],[Validators.required, Validators.minLength(1)]) @Input() placeholderText: string = 'Selectionnez vos options dans la liste'; @Input() titleBtn: string = 'Choix des options'; @@ -36,20 +39,30 @@ export class BtnSelectComponent implements OnInit { listOptionChosen: string[] = []; configObjAdded: JsonData = {}; genericResponse: JsonData = {}; + objToEdit: JsonData; + @Input() bEdit: boolean; + @Input() isInitialValues:boolean; @Input() paramToFilt: string; @Input() callBackFunction: ( pageNumber: number, limit: number, valueToFilter: string ) => Observable; + @Input() initValueFunction : ()=> JsonData; @ViewChild('optionInput') optionInput: ElementRef; @Output() public sendobject = new EventEmitter(); - constructor() {} + constructor(private _formService: FormService) { } ngOnInit() { + + if(this.isInitialValues && !this.isInit){ + this.initFromExistingObj(this.paramToFilt) + this.objToEdit.map(val => this.addObject(val)) + this.isInit = true + } this.filteredOptions = this.myControl.valueChanges.pipe( startWith(''), debounceTime(400), @@ -63,6 +76,8 @@ export class BtnSelectComponent implements OnInit { }), map((res) => (res.length > 0 ? res : [{ name: 'Pas de résultats' }])) ); + this.listOpNeeded.setValue(this.listOptionChosen) + this._formService.changeExtraFormControl(this.listOpNeeded,"listOptBtnSelect") } remove(option: string): void { @@ -76,6 +91,8 @@ export class BtnSelectComponent implements OnInit { delete this.configObjAdded[option]; } this.sendobject.emit(this.configObjAdded); + this.listOpNeeded.setValue(this.listOptionChosen) + this._formService.changeExtraFormControl(this.listOpNeeded,"listOptBtnSelect") } selected(event: MatAutocompleteSelectedEvent): void { @@ -85,6 +102,8 @@ export class BtnSelectComponent implements OnInit { : null; this.optionInput.nativeElement.value = ''; this.myControl.setValue(null); + this.listOpNeeded.setValue(this.listOptionChosen) + this._formService.changeExtraFormControl(this.listOpNeeded,"listOptBtnSelect") } filterOnRequest(val: string, keyToFilt: string): Observable { @@ -120,4 +139,16 @@ export class BtnSelectComponent implements OnInit { this.configObjAdded[name] = configAndId; this.sendobject.emit(this.configObjAdded); } + + initFromExistingObj(keyToFilt: string){ + const objInput = this.initValueFunction() + this.objToEdit = objInput .filter((obj) => { + Object.assign(obj, { name: obj[keyToFilt] })[keyToFilt]; + delete obj[keyToFilt]; + return obj; + }) + this.objToEdit.map(obj => this.listOptionChosen.push(obj.name)) + + } + } diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html index 7f4f5f46e..27b66a108 100644 --- a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html @@ -18,7 +18,7 @@

Attention

-
+
diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts index 433aa6495..29a8578e9 100644 --- a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts @@ -11,7 +11,7 @@ import { IDataForm } from '../../interfaces/form'; import { ApiGeomService } from '../../services/api-geom.service'; import { ConfigJsonService } from '../../services/config-json.service'; import { FormService } from '../../services/form.service'; -import { ObjectService } from '../../services/object.service'; +import { IExtraForm } from '../../interfaces/object'; @Component({ selector: 'pnx-monitoring-form-g', @@ -34,6 +34,10 @@ export class MonitoringFormComponentG implements OnInit { @Input() sites: {}; @Input() apiService: ApiGeomService; + @Input() isExtraForm:boolean = false; + + extraForm: IExtraForm; + hideForm: boolean = false; dataForm: IDataForm; searchSite = ''; @@ -63,17 +67,21 @@ export class MonitoringFormComponentG implements OnInit { ) {} ngOnInit() { - // TODO: Avoid two subscribes one inside other (code test above doesn't work. When add type site the observable currentdata is not recall) this._formService.currentData .pipe( tap((data) => { this.obj = data; this.obj.bIsInitialized = true; - this.apiService.init(this.obj.endPoint, this.obj.objSelected); + this.obj.id = this.obj[this.obj.pk] }), - mergeMap((data: any) => this._configService.init(data.moduleCode)) + mergeMap((data: any) => this._configService.init(data.moduleCode)), + mergeMap(() => this._formService.currentExtraFormCtrl ) ) - .subscribe(() => { + .subscribe((frmCtrl) => { + + this.isExtraForm ? this.addExtraFormCtrl(frmCtrl) : null; + this.isExtraForm ? this.checkValidExtraFormCtrl() : null; + this.queryParams = this._route.snapshot.queryParams || {}; this.bChainInput = this._configService.frontendParams()['bChainInput']; @@ -294,7 +302,6 @@ export class MonitoringFormComponentG implements OnInit { onSubmit() { const { patch_update, ...sendValue } = this.dataForm; const objToUpdateOrCreate = this._formService.postData(sendValue, this.obj); - console.log(objToUpdateOrCreate); const action = this.obj.id ? this.apiService.patch(this.obj.id, objToUpdateOrCreate) : this.apiService.create(objToUpdateOrCreate); @@ -382,6 +389,25 @@ export class MonitoringFormComponentG implements OnInit { this.procesPatchUpdateForm(); } + addExtraFormCtrl(frmCtrl: IExtraForm){ + if (frmCtrl.frmName in this.objForm.controls){ + this.objForm.setControl(frmCtrl.frmName,frmCtrl.frmCtrl) + } else{ + this.objForm.addControl(frmCtrl.frmName,frmCtrl.frmCtrl) + } + + this.extraForm = frmCtrl + } + + checkValidExtraFormCtrl(){ + if (this.extraForm.frmName in this.objForm.controls && this.objForm.get(this.extraForm.frmName).value != null && this.objForm.get(this.extraForm.frmName).value.length != 0 ){ + this.hideForm = false + this.objForm.valid + } else { + this.hideForm = true + } +} + getConfigFromBtnSelect(event) { // this.obj.specific == undefined ? (this.obj.specific = {}) : null; // TODO: Ajout de tous les id_parents ["id_sites_groups" etc ] dans l'objet obj.dataComplement diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts index ec93302e3..d534aeee1 100644 --- a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts @@ -46,13 +46,13 @@ export class MonitoringPropertiesGComponent implements OnInit { } onEditClick() { - this.bEditChange.emit(true); this.selectedObj['id'] = this.selectedObj[this.selectedObj.pk]; this._formService.changeDataSub( this.selectedObj, this.objectType.objectType, this.objectType.endPoint ); + this.bEditChange.emit(true); } ngOnChanges(changes: SimpleChanges): void { diff --git a/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.html b/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.html index cd9a5bcd3..9f1edcf9f 100644 --- a/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.html +++ b/frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.html @@ -8,6 +8,7 @@
diff --git a/frontend/app/components/monitoring-visits/monitoring-visits.component.html b/frontend/app/components/monitoring-visits/monitoring-visits.component.html index a451e2484..db5d49214 100644 --- a/frontend/app/components/monitoring-visits/monitoring-visits.component.html +++ b/frontend/app/components/monitoring-visits/monitoring-visits.component.html @@ -1,10 +1,44 @@ -
+ + + + + + diff --git a/frontend/app/components/monitoring-visits/monitoring-visits.component.ts b/frontend/app/components/monitoring-visits/monitoring-visits.component.ts index 3cbc34118..d7a103dd2 100644 --- a/frontend/app/components/monitoring-visits/monitoring-visits.component.ts +++ b/frontend/app/components/monitoring-visits/monitoring-visits.component.ts @@ -1,11 +1,11 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { forkJoin } from 'rxjs'; +import { Observable, forkJoin } from 'rxjs'; import { map, mergeMap } from 'rxjs/operators'; import { MonitoringGeomComponent } from '../../class/monitoring-geom-component'; -import { ISite } from '../../interfaces/geom'; +import { ISite, ISiteType } from '../../interfaces/geom'; import { IPage, IPaginated } from '../../interfaces/page'; import { IVisit } from '../../interfaces/visit'; import { SitesService, VisitsService } from '../../services/api-geom.service'; @@ -15,7 +15,7 @@ import { JsonData } from '../../types/jsondata'; import { SelectObject } from '../../interfaces/object'; import { Module } from '../../interfaces/module'; import { ConfigService } from '../../services/config.service'; - +import { FormService } from "../../services/form.service"; @Component({ selector: 'monitoring-visits', templateUrl: './monitoring-visits.component.html', @@ -26,13 +26,22 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement @Input() visits: IVisit[]; @Input() page: IPage; // colsname: typeof columnNameVisit = columnNameVisit; - objectType: string; - bEdit: boolean; - objForm: FormGroup; - @Input() colsname; + @Input() bEdit: boolean; + form: FormGroup; + colsname: {}; objParent: any; modules: SelectObject[]; + isInitialValues:boolean; + paramToFilt: string = 'label'; + funcToFilt: Function; + funcInitValues: Function; + titleBtn: string = 'Choix des types de sites'; + placeholderText: string = 'Sélectionnez les types de site'; + id_sites_group: number; + types_site: string[]; + config: JsonData; + constructor( private _sites_service: SitesService, private _visits_service: VisitsService, @@ -41,36 +50,47 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement private router: Router, private _Activatedroute: ActivatedRoute, private _formBuilder: FormBuilder, - private _configService: ConfigService + private _formService: FormService, + private _configService: ConfigService, + private siteService: SitesService, ) { super(); this.getAllItemsCallback = this.getVisits; - this.objectType = 'sites'; } ngOnInit() { - this.objForm = this._formBuilder.group({}); + this.funcInitValues = this.initValueToSend.bind(this) + this.funcToFilt = this.partialfuncToFilt.bind(this); + this.form = this._formBuilder.group({}); this._objService.changeObjectTypeParent(this._sites_service.objectObs, true); this._objService.currentObjectTypeParent.subscribe((objParent) => (this.objParent = objParent)); this._objService.changeObjectType(this._visits_service.objectObs); + this.initSiteVisit() + + } + + initSiteVisit(){ this._Activatedroute.params - .pipe( - map((params) => params['id'] as number), - mergeMap((id: number) => - forkJoin({ - site: this._sites_service.getById(id), - visits: this._visits_service.get(1, this.limit, { - id_base_site: id, - }), - }) - ) + .pipe( + map((params) => params['id'] as number), + mergeMap((id: number) => + forkJoin({ + site: this._sites_service.getById(id), + visits: this._visits_service.get(1, this.limit, { + id_base_site: id, + }), + }) ) - .subscribe((data: { site: ISite; visits: IPaginated }) => { - this.site = data.site; - this.setVisits(data.visits); - this.baseFilters = { id_base_site: this.site.id_base_site }; - }); + ) + .subscribe((data: { site: ISite; visits: IPaginated }) => { + this._objService.changeSelectedObj(data.site, true); + this.site = data.site; + this.types_site = data.site['types_site'] + this.setVisits(data.visits); + this.baseFilters = { id_base_site: this.site.id_base_site }; + }); + this.isInitialValues = true; } getVisits(page: number, filters: JsonData) { @@ -115,4 +135,60 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement }); }); } + partialfuncToFilt( + pageNumber: number, + limit: number, + valueToFilter: string + ): Observable> { + return this.siteService.getTypeSites(pageNumber, limit, { + label_fr: valueToFilter, + sort_dir: 'desc', + }); + } + + onSendConfig(config: JsonData): void { + this.config = this.addTypeSiteListIds(config); + this.updateForm() + // this.monitoringFormComponentG.getConfigFromBtnSelect(this.config); + } + + addTypeSiteListIds(config: JsonData): JsonData { + if (config && config.length != 0) { + config.types_site = []; + for (const key in config) { + if ('id_nomenclature_type_site' in config[key]) { + config.types_site.push(config[key]['id_nomenclature_type_site']); + } + } + } + return config; + } + + initValueToSend(){ + this.initSiteVisit() + return this.types_site + } + + updateForm(){ + this.site.specific = {}; + this.site.dataComplement = {}; + for (const key in this.config) { + if (this.config[key].config != undefined) { + if (Object.keys(this.config[key].config).length !== 0) { + Object.assign(this.site.specific, this.config[key].config.specific); + } + } + } + for(const k in this.site.data) this.site[k]=this.site.data[k]; + this.site.types_site = this.config.types_site + Object.assign(this.site.dataComplement, this.config); + + this._formService.changeDataSub(this.site, + this.objParent.objectType, + this.objParent.endPoint); + } + + onObjChanged($event) { + this.initSiteVisit(); + } } diff --git a/frontend/app/gnModule.module.ts b/frontend/app/gnModule.module.ts index 6ef0e9801..9934e7143 100644 --- a/frontend/app/gnModule.module.ts +++ b/frontend/app/gnModule.module.ts @@ -55,7 +55,7 @@ import { BtnSelectComponent } from "./components/btn-select/btn-select.component import { MonitoringSitesEditComponent } from "./components/monitoring-sites-edit/monitoring-sites-edit.component"; import { MonitoringVisitsComponent } from "./components/monitoring-visits/monitoring-visits.component"; import { OptionListButtonComponent } from "./components/option-list-btn/option-list-btn.component"; - +import { MatErrorMessagesDirective } from './utils/matErrorMessages.directive'; // my module routing const routes: Routes = [ /** modules */ @@ -137,6 +137,7 @@ const routes: Routes = [ BtnSelectComponent, MonitoringVisitsComponent, OptionListButtonComponent, + MatErrorMessagesDirective ], imports: [ GN2CommonModule, diff --git a/frontend/app/interfaces/geom.ts b/frontend/app/interfaces/geom.ts index de69a3b40..0d6fe7ed0 100644 --- a/frontend/app/interfaces/geom.ts +++ b/frontend/app/interfaces/geom.ts @@ -34,6 +34,9 @@ export interface ISite extends IGeomObject { meta_update_date: Date; nb_visits: number; uuid_base_site: string; + specific: JsonData; + dataComplement: JsonData; + types_site: string[]; } export interface IGeomService extends IService { diff --git a/frontend/app/interfaces/object.ts b/frontend/app/interfaces/object.ts index cec2af00d..167ac95e5 100644 --- a/frontend/app/interfaces/object.ts +++ b/frontend/app/interfaces/object.ts @@ -1,7 +1,9 @@ -import { JsonData } from "../types/jsondata"; -import { IPaginated } from "./page"; -import { GeoJSON } from "geojson"; -import { Observable } from "rxjs"; +import { AbstractControl } from '@angular/forms'; +import { GeoJSON } from 'geojson'; +import { Observable } from 'rxjs'; + +import { JsonData } from '../types/jsondata'; +import { IPaginated } from './page'; export interface IObject { data: JsonData; @@ -23,3 +25,4 @@ export type SelectObject = { label: string; }; +export type IExtraForm = { frmCtrl: AbstractControl; frmName: string }; diff --git a/frontend/app/services/form.service.ts b/frontend/app/services/form.service.ts index c8d6b0ff5..0798b10df 100644 --- a/frontend/app/services/form.service.ts +++ b/frontend/app/services/form.service.ts @@ -3,23 +3,29 @@ import { BehaviorSubject, Observable, forkJoin, of } from 'rxjs'; import { concatMap } from 'rxjs/operators'; import { ISite, ISitesGroup } from '../interfaces/geom'; -import { IobjObs, ObjDataType } from '../interfaces/objObs'; import { JsonData } from '../types/jsondata'; import { Utils } from '../utils/utils'; import { MonitoringObjectService } from './monitoring-object.service'; +import { FormControl } from '@angular/forms'; +import { IExtraForm } from '../interfaces/object'; + @Injectable() export class FormService { data: JsonData = {}; + frmCtrl: FormControl = new FormControl(null); + frmCtrlName: string = ''; private dataSub = new BehaviorSubject(this.data); + private formCtrl = new BehaviorSubject({frmCtrl : this.frmCtrl,frmName:this.frmCtrlName}); currentData = this.dataSub.asObservable(); + currentExtraFormCtrl = this.formCtrl.asObservable(); properties: JsonData = {}; moduleCode: string; objecType: string; constructor(private _objService: MonitoringObjectService) {} - // TODO: voir si nécessaire de garder ça (objService permet d'avoir le bon objet ? et sinon modifier pour obtenir ce qu'il faut en formulaire) + changeDataSub( newDat: JsonData, objectType: string, @@ -40,6 +46,10 @@ export class FormService { this.dataSub.next(newDat); } + changeExtraFormControl(formCtrl:FormControl,formCtrlName:string){ + this.formCtrl.next({frmCtrl:formCtrl,frmName:formCtrlName}) + } + formValues(obj): Observable { // const {properties ,remainaing} = obj const properties = Utils.copy(this.properties); diff --git a/frontend/app/utils/matErrorMessages.directive.ts b/frontend/app/utils/matErrorMessages.directive.ts new file mode 100644 index 000000000..66d8b53c8 --- /dev/null +++ b/frontend/app/utils/matErrorMessages.directive.ts @@ -0,0 +1,37 @@ +import { Component, AfterViewInit, Injector } from '@angular/core'; +import {MatInput } from '@angular/material/input'; +import { MatFormFieldControl,MatFormField } from '@angular/material/form-field'; + +@Component({ + selector: '[matErrorMessages]', + template: '{{ error }}' +}) +export class MatErrorMessagesDirective implements AfterViewInit { + error = ''; + inputRef: MatFormFieldControl; + + constructor(private _inj: Injector) { } + + // Setup all initial tooling + ngAfterViewInit() { + // grab reference to MatFormField directive, where form control is accessible. + let container = this._inj.get(MatFormField); + this.inputRef = container._control; + + // sub to the control's status stream + this.inputRef.ngControl.statusChanges.subscribe(this.updateErrors); + } + + // This grabs a single active error instead of multiple. + private updateErrors = (state: 'VALID' | 'INVALID') => { + if (state === 'INVALID') { + let controlErrors = this.inputRef.ngControl.errors; + const firstError = Object.keys(controlErrors)[0]; + if(firstError === 'required') + this.error = 'Ce champs est requis.'; + + if(firstError === 'minlength') + this.error = 'Vous devez choisir au moins une valeur.'; + } + } +} \ No newline at end of file From 50a3ea8df4a0a9d8b144735bf25a504962c77e0d Mon Sep 17 00:00:00 2001 From: andriacap <111564663+andriacap@users.noreply.github.com> Date: Thu, 4 May 2023 10:37:09 +0200 Subject: [PATCH 040/161] style: formatter json in flask admin (#63) - Create jsonformatter inside utils.py - Add formatter to column "config" Reviewed-by: andriac [Refs_ticket] : #59 --- backend/gn_module_monitoring/monitoring/admin.py | 6 +++--- backend/gn_module_monitoring/monitoring/utils.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/backend/gn_module_monitoring/monitoring/admin.py b/backend/gn_module_monitoring/monitoring/admin.py index aec375e21..0fcdc14df 100644 --- a/backend/gn_module_monitoring/monitoring/admin.py +++ b/backend/gn_module_monitoring/monitoring/admin.py @@ -1,11 +1,11 @@ from flask_admin.contrib.sqla import ModelView from geonature.core.admin.admin import CruvedProtectedMixin from geonature.utils.env import DB -from pypnnomenclature.models import TNomenclatures, BibNomenclaturesTypes +from pypnnomenclature.models import BibNomenclaturesTypes, TNomenclatures from wtforms.validators import ValidationError from gn_module_monitoring.monitoring.models import BibTypeSite - +from gn_module_monitoring.monitoring.utils import json_formatter SITE_TYPE = "TYPE_SITE" @@ -72,5 +72,5 @@ def list_label_nomenclature_formatter(view, _context, model, _name): ) column_list = ("nomenclature", "config") - column_formatters = dict(nomenclature=list_label_nomenclature_formatter) + column_formatters = dict(nomenclature=list_label_nomenclature_formatter, config=json_formatter) form_excluded_columns = "sites" diff --git a/backend/gn_module_monitoring/monitoring/utils.py b/backend/gn_module_monitoring/monitoring/utils.py index e69de29bb..05f0dc18b 100644 --- a/backend/gn_module_monitoring/monitoring/utils.py +++ b/backend/gn_module_monitoring/monitoring/utils.py @@ -0,0 +1,10 @@ +import json + +from jinja2.utils import markupsafe + + +def json_formatter(view, context, model, name): + """Prettify JSON data in flask admin lists""" + value = getattr(model, name) + json_value = json.dumps(value, ensure_ascii=False, indent=2) + return markupsafe.Markup("
{}
".format(json_value)) From 284fa28a842e92211d1d3b5e5b453b2240fb5716 Mon Sep 17 00:00:00 2001 From: Maxime Vergez Date: Thu, 4 May 2023 11:56:41 +0200 Subject: [PATCH 041/161] feat(front): try to make breadcrumb work In the site entry part --- .../breadcrumbs/breadcrumbs.component.ts | 34 ++++++++++++++++--- .../monitoring-map-list.component.html | 3 +- .../monitoring-map-list.component.ts | 14 +++++--- frontend/app/interfaces/objObs.ts | 5 +++ 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/frontend/app/components/breadcrumbs/breadcrumbs.component.ts b/frontend/app/components/breadcrumbs/breadcrumbs.component.ts index 0465da827..c42e5f3e5 100644 --- a/frontend/app/components/breadcrumbs/breadcrumbs.component.ts +++ b/frontend/app/components/breadcrumbs/breadcrumbs.component.ts @@ -6,9 +6,11 @@ import { Component, OnInit, Input, Output, SimpleChanges, EventEmitter } from '@ import { DataMonitoringObjectService } from '../../services/data-monitoring-object.service'; import { ConfigService } from '../../services/config.service'; -import { MonitoringObject } from '../../class/monitoring-object'; -import { Router } from '@angular/router'; -import { ActivatedRoute } from '@angular/router'; +import { MonitoringObject } from "../../class/monitoring-object"; +import { Router } from "@angular/router"; +import { ActivatedRoute } from "@angular/router"; +import { ObjectService } from "../../services/object.service"; +import { SiteSiteGroup } from "../../interfaces/objObs"; @Component({ selector: 'pnx-monitoring-breadcrumbs', @@ -24,17 +26,38 @@ export class BreadcrumbsComponent implements OnInit { public frontendModuleMonitoringUrl: string; @Input() obj: MonitoringObject; + // Specific to the site access + siteSiteGroup: SiteSiteGroup | null = null; constructor( private _dataMonitoringObjectService: DataMonitoringObjectService, private _configService: ConfigService, private _router: Router, - private _route: ActivatedRoute + private _route: ActivatedRoute, + private _objectService: ObjectService ) {} ngOnInit() { - // this.initBreadcrumbs(); + if (this.obj === undefined) { + this._objectService.currentObjectTypeParent.subscribe((parent) => { + console.log(this.siteSiteGroup) + if (parent.schema) { + if (parent.objectType == 'sites_group') { + this.siteSiteGroup = { + siteGroup: parent, + site: null, + }; + } else if (parent.objectType == 'site' && this.siteSiteGroup?.siteGroup) { + this.siteSiteGroup = { + ...this.siteSiteGroup, + site: parent, + }; + } + } + }); + } } + // this.initBreadcrumbs(); initBreadcrumbs() { if (this.obj.deleted) { @@ -87,6 +110,7 @@ export class BreadcrumbsComponent implements OnInit { } ngOnChanges(changes: SimpleChanges) { + console.log(this.siteSiteGroup) for (const propName of Object.keys(changes)) { const chng = changes[propName]; const cur = chng.currentValue; diff --git a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html index ee93b5280..864ccd791 100644 --- a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html +++ b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html @@ -6,8 +6,9 @@
+ - +
\ No newline at end of file diff --git a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts index 65bf3f50d..9c3d82b7b 100644 --- a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts +++ b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts @@ -1,11 +1,17 @@ -import { Component } from "@angular/core"; +import { Component } from '@angular/core'; +import { SiteSiteGroup } from '../../interfaces/objObs'; +import { MonitoringSitesComponent } from '../monitoring-sites/monitoring-sites.component'; @Component({ - selector: "monitoring-map-list.component", - templateUrl: "./monitoring-map-list.component.html", - styleUrls: ["./monitoring-map-list.component.css"], + selector: 'monitoring-map-list.component', + templateUrl: './monitoring-map-list.component.html', + styleUrls: ['./monitoring-map-list.component.css'], }) export class MonitoringMapListComponent { displayMap: boolean = true; + siteSiteGroup: SiteSiteGroup | null = null; constructor() {} + + onActivate(component) { + } } diff --git a/frontend/app/interfaces/objObs.ts b/frontend/app/interfaces/objObs.ts index a4a28c5de..a6dbd1aaf 100644 --- a/frontend/app/interfaces/objObs.ts +++ b/frontend/app/interfaces/objObs.ts @@ -24,3 +24,8 @@ export interface IobjObs { }; dataTable: { colNameObj: {} }; } + +export interface SiteSiteGroup { + site: IobjObs | null, + siteGroup: IobjObs +} \ No newline at end of file From 2e138a1607befd392df53841a11d1812424b6ab8 Mon Sep 17 00:00:00 2001 From: Andria Capai Date: Mon, 15 May 2023 18:39:59 +0200 Subject: [PATCH 042/161] feat(front): make breadcrumn logic inside component - Get all information from loading component to build breadcrumb information - Fix call twice siteService (delete the one with snake_case) - Use logic from observable and snapshot url to satisfy all case to access to route (by component, directly by url etc) Reviewed-by: andriacap --- .../breadcrumbs/breadcrumbs.component.ts | 64 ++++---- .../monitoring-map-list.component.html | 2 + .../monitoring-sites.component.html | 1 + .../monitoring-sites.component.ts | 62 +++++--- ...monitoring-sitesgroups-create.component.ts | 4 +- .../monitoring-sitesgroups.component.ts | 10 ++ .../monitoring-visits.component.ts | 143 ++++++++++++------ frontend/app/interfaces/object.ts | 9 ++ frontend/app/services/object.service.ts | 64 +++++--- 9 files changed, 241 insertions(+), 118 deletions(-) diff --git a/frontend/app/components/breadcrumbs/breadcrumbs.component.ts b/frontend/app/components/breadcrumbs/breadcrumbs.component.ts index c42e5f3e5..26077cef3 100644 --- a/frontend/app/components/breadcrumbs/breadcrumbs.component.ts +++ b/frontend/app/components/breadcrumbs/breadcrumbs.component.ts @@ -11,6 +11,13 @@ import { Router } from "@angular/router"; import { ActivatedRoute } from "@angular/router"; import { ObjectService } from "../../services/object.service"; import { SiteSiteGroup } from "../../interfaces/objObs"; +import { IBreadCrumb } from "../../interfaces/object"; + +export const breadCrumbElementBase: IBreadCrumb = { + "description":"Liste des groupes de site", + "label":"", + "url": "sites_group" +} @Component({ selector: 'pnx-monitoring-breadcrumbs', @@ -18,13 +25,13 @@ import { SiteSiteGroup } from "../../interfaces/objObs"; styleUrls: ['./breadcrumbs.component.css'], }) export class BreadcrumbsComponent implements OnInit { - public breadcrumbs; - + public breadcrumbs: IBreadCrumb[] =[]; @Input() bEdit: boolean; @Output() bEditChange = new EventEmitter(); public frontendModuleMonitoringUrl: string; - + public newLabel: string; + public new_desc: string; @Input() obj: MonitoringObject; // Specific to the site access siteSiteGroup: SiteSiteGroup | null = null; @@ -39,25 +46,12 @@ export class BreadcrumbsComponent implements OnInit { ngOnInit() { if (this.obj === undefined) { - this._objectService.currentObjectTypeParent.subscribe((parent) => { - console.log(this.siteSiteGroup) - if (parent.schema) { - if (parent.objectType == 'sites_group') { - this.siteSiteGroup = { - siteGroup: parent, - site: null, - }; - } else if (parent.objectType == 'site' && this.siteSiteGroup?.siteGroup) { - this.siteSiteGroup = { - ...this.siteSiteGroup, - site: parent, - }; - } - } - }); + this._objectService.currentDataBreadCrumb.subscribe( + (breadCrumb) => (this.breadcrumbs = breadCrumb) + ); + return; } } - // this.initBreadcrumbs(); initBreadcrumbs() { if (this.obj.deleted) { @@ -91,18 +85,23 @@ export class BreadcrumbsComponent implements OnInit { this.bEditChange.emit(false); setTimeout(() => { if (elem) { - this._router.navigate( - [ - this._configService.frontendModuleMonitoringUrl(), - 'object', - elem.module_code, - elem.object_type, - elem.id, - ], - { - queryParams: elem.params, - } - ); + if (this.obj == undefined) { + const url = [this._configService.frontendModuleMonitoringUrl(), elem.url].join('/'); + this._router.navigateByUrl(url); + } else { + this._router.navigate( + [ + this._configService.frontendModuleMonitoringUrl(), + 'object', + elem.module_code, + elem.object_type, + elem.id, + ], + { + queryParams: elem.params, + } + ); + } } else { this._router.navigate([this._configService.frontendModuleMonitoringUrl()]); } @@ -110,7 +109,6 @@ export class BreadcrumbsComponent implements OnInit { } ngOnChanges(changes: SimpleChanges) { - console.log(this.siteSiteGroup) for (const propName of Object.keys(changes)) { const chng = changes[propName]; const cur = chng.currentValue; diff --git a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html index 864ccd791..a47a1e602 100644 --- a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html +++ b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html @@ -6,9 +6,11 @@
+
+
\ No newline at end of file diff --git a/frontend/app/components/monitoring-sites/monitoring-sites.component.html b/frontend/app/components/monitoring-sites/monitoring-sites.component.html index 943d69b13..cc9eb58b4 100644 --- a/frontend/app/components/monitoring-sites/monitoring-sites.component.html +++ b/frontend/app/components/monitoring-sites/monitoring-sites.component.html @@ -1,3 +1,4 @@ + ; objParent: any; - + breadCrumbElemnt: IBreadCrumb = { label: 'Groupe de site', description: '' }; + breadCrumbElementBase: IBreadCrumb = breadCrumbElementBase; + breadCrumbList: IBreadCrumb[] = []; constructor( public _sitesGroupService: SitesGroupService, private _siteService: SitesService, @@ -76,25 +80,28 @@ export class MonitoringSitesComponent extends MonitoringGeomComponent implements id_sites_group: id, }), }) - )) - .subscribe( - (data: { sitesGroup: ISitesGroup; sites: IPaginated}) => { - this._objService.changeSelectedObj(data.sitesGroup, true); - this.sitesGroup = data.sitesGroup; - this.sites = data.sites.items; - this.page = { - count: data.sites.count, - page: data.sites.page, - limit: data.sites.limit, - }; - this.siteGroupLayer = this._geojsonService.setMapData( - data.sitesGroup.geometry, - () => {} - ); - this.baseFilters = { id_sites_group: this.sitesGroup.id_sites_group }; - this.colsname = this._siteService.objectObs.dataTable.colNameObj; - } - ); + ) + ) + .subscribe((data) => { + console.log(data); + this._objService.changeSelectedObj(data.sitesGroup, true); + this._objService.changeSelectedParentObj(data.sitesGroup, true); + this.sitesGroup = data.sitesGroup; + this.sites = data.sites.items; + this.page = { + count: data.sites.count, + page: data.sites.page, + limit: data.sites.limit, + }; + // this.siteGroupLayer = this._geojsonService.setMapData( + // data.sitesGroup.geometry, + // () => {} + // ); + this.baseFilters = { id_sites_group: this.sitesGroup.id_sites_group }; + this.colsname = this._siteService.objectObs.dataTable.colNameObj; + this._objService.changeSelectedParentObj(data.sitesGroup, true); + this.updateBreadCrumb(data.sitesGroup); + }); } ngOnDestroy() { this._geojsonService.removeFeatureGroup(this._geojsonService.sitesFeatureGroup); @@ -133,6 +140,21 @@ export class MonitoringSitesComponent extends MonitoringGeomComponent implements }); } + updateBreadCrumb(sitesGroup) { + this.breadCrumbElemnt.description = sitesGroup.sites_group_name; + this.breadCrumbElemnt.label = 'Groupe de site'; + this.breadCrumbElemnt['id'] = sitesGroup.id_sites_group; + this.breadCrumbElemnt['objectType'] = + this._sitesGroupService.objectObs.objectType || 'sites_group'; + this.breadCrumbElemnt['url'] = [ + this.breadCrumbElementBase.url, + this.breadCrumbElemnt.id?.toString(), + ].join('/'); + + this.breadCrumbList = [this.breadCrumbElementBase, this.breadCrumbElemnt]; + this._objService.changeBreadCrumb(this.breadCrumbList, true); + } + onObjChanged($event) { this.initSite(); } diff --git a/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts index 7ff9a6791..e4107df2a 100644 --- a/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts +++ b/frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts @@ -1,11 +1,11 @@ import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; import { endPoints } from '../../enum/endpoints'; import { ISitesGroup } from '../../interfaces/geom'; import { FormService } from '../../services/form.service'; import { SitesGroupService } from '../../services/api-geom.service'; +import { ObjectService } from '../../services/object.service'; @Component({ selector: 'monitoring-sitesgroups-create', @@ -19,6 +19,7 @@ export class MonitoringSitesGroupsCreateComponent implements OnInit { constructor( private _formService: FormService, private _formBuilder: FormBuilder, + private _objService: ObjectService, public sitesGroupService: SitesGroupService ) {} @@ -35,6 +36,7 @@ export class MonitoringSitesGroupsCreateComponent implements OnInit { }, this.urlRelative ); + this._objService.changeSelectedObj({},true) this.form = this._formBuilder.group({}); } } diff --git a/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts b/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts index a940fc589..eb7cae7c9 100644 --- a/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts +++ b/frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts @@ -10,6 +10,8 @@ import { ObjectService } from "../../services/object.service"; import { FormGroup, FormBuilder } from "@angular/forms"; import { IobjObs } from "../../interfaces/objObs"; import { ConfigJsonService } from "../../services/config-json.service"; +import { IBreadCrumb } from "../../interfaces/object"; +import { breadCrumbElementBase } from "../breadcrumbs/breadcrumbs.component"; const LIMIT = 10; @@ -33,6 +35,7 @@ export class MonitoringSitesGroupsComponent objectType: IobjObs; objForm: FormGroup; objInitForm: Object = {}; + breadCrumbElementBase: IBreadCrumb = breadCrumbElementBase; // siteGroupEmpty={ // "comments" :'', // sites_group_code: string; @@ -56,6 +59,7 @@ export class MonitoringSitesGroupsComponent ngOnInit() { this.initSiteGroup(); + this._objService.changeSelectedObj({}, true); } initSiteGroup() { @@ -102,6 +106,7 @@ export class MonitoringSitesGroupsComponent }; this.sitesGroups = data.items; this.colsname = this._sites_group_service.objectObs.dataTable.colNameObj; + this.updateBreadCrumb(); // IF prefered observable compare to ngOnChanges uncomment this: // this._dataTableService.changeColsTable(this.colsname,this.sitesGroups[0]) }); @@ -122,6 +127,11 @@ export class MonitoringSitesGroupsComponent relativeTo: this._Activatedroute, }); } + + updateBreadCrumb() { + this._objService.changeBreadCrumb([this.breadCrumbElementBase], true); + } + onSelect($event) { this.geojsonService.selectSitesGroupLayer($event); } diff --git a/frontend/app/components/monitoring-visits/monitoring-visits.component.ts b/frontend/app/components/monitoring-visits/monitoring-visits.component.ts index d7a103dd2..adbb1694d 100644 --- a/frontend/app/components/monitoring-visits/monitoring-visits.component.ts +++ b/frontend/app/components/monitoring-visits/monitoring-visits.component.ts @@ -1,21 +1,22 @@ import { Component, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { Observable, forkJoin } from 'rxjs'; -import { map, mergeMap } from 'rxjs/operators'; +import { Observable, forkJoin, iif, of } from 'rxjs'; +import { map, mergeMap, take } from 'rxjs/operators'; import { MonitoringGeomComponent } from '../../class/monitoring-geom-component'; import { ISite, ISiteType } from '../../interfaces/geom'; import { IPage, IPaginated } from '../../interfaces/page'; import { IVisit } from '../../interfaces/visit'; -import { SitesService, VisitsService } from '../../services/api-geom.service'; +import { SitesGroupService, SitesService, VisitsService } from '../../services/api-geom.service'; import { GeoJSONService } from '../../services/geojson.service'; import { ObjectService } from '../../services/object.service'; import { JsonData } from '../../types/jsondata'; -import { SelectObject } from '../../interfaces/object'; +import { IBreadCrumb, SelectObject } from '../../interfaces/object'; import { Module } from '../../interfaces/module'; import { ConfigService } from '../../services/config.service'; import { FormService } from "../../services/form.service"; +import { breadCrumbElementBase } from '../breadcrumbs/breadcrumbs.component'; @Component({ selector: 'monitoring-visits', templateUrl: './monitoring-visits.component.html', @@ -32,7 +33,7 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement objParent: any; modules: SelectObject[]; - isInitialValues:boolean; + isInitialValues: boolean; paramToFilt: string = 'label'; funcToFilt: Function; funcInitValues: Function; @@ -41,9 +42,14 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement id_sites_group: number; types_site: string[]; config: JsonData; + siteGroupIdParent: number; + breadCrumbParent: IBreadCrumb = { label: 'Groupe de site', description: '' }; + breadCrumbChild: IBreadCrumb = { label: 'Site', description: '' }; + breadCrumbElementBase: IBreadCrumb = breadCrumbElementBase; + breadCrumbList: IBreadCrumb[] = []; constructor( - private _sites_service: SitesService, + private _sitesGroupService: SitesGroupService, private _visits_service: VisitsService, private _objService: ObjectService, public geojsonService: GeoJSONService, @@ -52,44 +58,71 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement private _formBuilder: FormBuilder, private _formService: FormService, private _configService: ConfigService, - private siteService: SitesService, + private _siteService: SitesService ) { super(); this.getAllItemsCallback = this.getVisits; } ngOnInit() { - this.funcInitValues = this.initValueToSend.bind(this) + this.funcInitValues = this.initValueToSend.bind(this); this.funcToFilt = this.partialfuncToFilt.bind(this); this.form = this._formBuilder.group({}); - this._objService.changeObjectTypeParent(this._sites_service.objectObs, true); - this._objService.currentObjectTypeParent.subscribe((objParent) => (this.objParent = objParent)); + this._objService.changeObjectTypeParent(this._siteService.objectObs, true); + this._objService.currentObjectTypeParent + .pipe(take(1)) + .subscribe((objParent) => (this.objParent = objParent)); this._objService.changeObjectType(this._visits_service.objectObs); - this.initSiteVisit() - + + this.siteGroupIdParent = parseInt( + this._Activatedroute.pathFromRoot[this._Activatedroute.pathFromRoot.length - 2].snapshot + .params['id'] + ); + this.initSiteVisit(); } - initSiteVisit(){ + initSiteVisit() { this._Activatedroute.params - .pipe( - map((params) => params['id'] as number), - mergeMap((id: number) => - forkJoin({ - site: this._sites_service.getById(id), - visits: this._visits_service.get(1, this.limit, { - id_base_site: id, - }), + .pipe( + map((params) => params['id'] as number), + mergeMap((id: number) => + forkJoin({ + site: this._siteService.getById(id), + visits: this._visits_service.get(1, this.limit, { + id_base_site: id, + }), + }) + ), + mergeMap((data: { site: ISite; visits: IPaginated }) => { + return this._objService.currentParentObjSelected.pipe( + take(1), + map((objSelectParent) => { + return { site: data.site, visits: data.visits, parentObjSelected: objSelectParent }; + }) + ); + }), + mergeMap((data) => { + return iif( + () => data.parentObjSelected == this.siteGroupIdParent, + of(data), + this._sitesGroupService.getById(this.siteGroupIdParent).pipe( + map((objSelectParent) => { + return { site: data.site, visits: data.visits, parentObjSelected: objSelectParent }; + }) + ) + ); }) ) - ) - .subscribe((data: { site: ISite; visits: IPaginated }) => { - this._objService.changeSelectedObj(data.site, true); - this.site = data.site; - this.types_site = data.site['types_site'] - this.setVisits(data.visits); - this.baseFilters = { id_base_site: this.site.id_base_site }; - }); + .subscribe((data) => { + this._objService.changeSelectedObj(data.site, true); + this.site = data.site; + this.types_site = data.site['types_site']; + this.setVisits(data.visits); + this.baseFilters = { id_base_site: this.site.id_base_site }; + + this.updateBreadCrumb(data.site, data.parentObjSelected); + }); this.isInitialValues = true; } @@ -116,7 +149,7 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement } getModules() { - this._sites_service.getSiteModules(this.site.id_base_site).subscribe( + this._siteService.getSiteModules(this.site.id_base_site).subscribe( (data: Module[]) => (this.modules = data.map((item) => { return { id: item.module_code, label: item.module_label }; @@ -128,8 +161,8 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement const moduleCode = $event.id; //create_object/cheveches_sites_group/visit?id_base_site=47 this._configService.init(moduleCode).subscribe(() => { - const keys = Object.keys(this._configService.config()[moduleCode]) - const parent_paths = ["sites_group", "site"].filter(item => keys.includes(item)) + const keys = Object.keys(this._configService.config()[moduleCode]); + const parent_paths = ['sites_group', 'site'].filter((item) => keys.includes(item)); this.router.navigate([`monitorings/create_object/${moduleCode}/visit`], { queryParams: { id_base_site: this.site.id_base_site, parents_path: parent_paths }, }); @@ -140,7 +173,7 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement limit: number, valueToFilter: string ): Observable> { - return this.siteService.getTypeSites(pageNumber, limit, { + return this._siteService.getTypeSites(pageNumber, limit, { label_fr: valueToFilter, sort_dir: 'desc', }); @@ -148,7 +181,7 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement onSendConfig(config: JsonData): void { this.config = this.addTypeSiteListIds(config); - this.updateForm() + this.updateForm(); // this.monitoringFormComponentG.getConfigFromBtnSelect(this.config); } @@ -164,12 +197,12 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement return config; } - initValueToSend(){ - this.initSiteVisit() - return this.types_site + initValueToSend() { + this.initSiteVisit(); + return this.types_site; } - updateForm(){ + updateForm() { this.site.specific = {}; this.site.dataComplement = {}; for (const key in this.config) { @@ -179,13 +212,37 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement } } } - for(const k in this.site.data) this.site[k]=this.site.data[k]; - this.site.types_site = this.config.types_site + for (const k in this.site.data) this.site[k] = this.site.data[k]; + this.site.types_site = this.config.types_site; Object.assign(this.site.dataComplement, this.config); - this._formService.changeDataSub(this.site, - this.objParent.objectType, - this.objParent.endPoint); + this._formService.changeDataSub(this.site, this.objParent.objectType, this.objParent.endPoint); + } + + updateBreadCrumb(site, parentSelected) { + this.breadCrumbParent.description = parentSelected.sites_group_name; + this.breadCrumbParent.label = 'Groupe de site'; + this.breadCrumbParent['id'] = parentSelected.id_sites_group; + this.breadCrumbParent['objectType'] = + this._sitesGroupService.objectObs.objectType || 'sites_group'; + this.breadCrumbParent['url'] = [ + this.breadCrumbElementBase.url, + this.breadCrumbParent.id?.toString(), + ].join('/'); + + this.breadCrumbChild.description = site.base_site_name; + this.breadCrumbChild.label = 'Site'; + this.breadCrumbChild['id'] = site.id_base_site; + this.breadCrumbChild['objectType'] = this._siteService.objectObs.objectType || 'site'; + this.breadCrumbChild['url'] = [ + this.breadCrumbElementBase.url, + this.breadCrumbParent.id?.toString(), + this.breadCrumbChild.objectType, + this.breadCrumbChild.id?.toString(), + ].join('/'); + + this.breadCrumbList = [this.breadCrumbElementBase, this.breadCrumbParent, this.breadCrumbChild]; + this._objService.changeBreadCrumb(this.breadCrumbList, true); } onObjChanged($event) { diff --git a/frontend/app/interfaces/object.ts b/frontend/app/interfaces/object.ts index 167ac95e5..2824b44a6 100644 --- a/frontend/app/interfaces/object.ts +++ b/frontend/app/interfaces/object.ts @@ -26,3 +26,12 @@ export type SelectObject = { }; export type IExtraForm = { frmCtrl: AbstractControl; frmName: string }; + +export type IBreadCrumb = { + label: string; + description: string; + id?:number; + url?:string; + objectType?:string; + params?:JsonData; +} diff --git a/frontend/app/services/object.service.ts b/frontend/app/services/object.service.ts index 58b183f7b..c06d83c0a 100644 --- a/frontend/app/services/object.service.ts +++ b/frontend/app/services/object.service.ts @@ -1,53 +1,75 @@ import { Injectable } from "@angular/core"; -import { BehaviorSubject,ReplaySubject } from "rxjs"; -import { endPoints } from "../enum/endpoints"; -import { ISitesGroup, ISiteType } from "../interfaces/geom"; +import { ReplaySubject } from "rxjs"; import { IobjObs, ObjDataType } from "../interfaces/objObs"; import { JsonData } from "../types/jsondata"; - +import { IBreadCrumb } from "../interfaces/object"; @Injectable() export class ObjectService { objObs: IobjObs; - private objSelected = new ReplaySubject(1); + private objSelected = new ReplaySubject(1); currentObjSelected = this.objSelected.asObservable(); + private parentObjSelected = new ReplaySubject(1); + currentParentObjSelected = this.parentObjSelected.asObservable(); + private dataObjType = new ReplaySubject>(1); currentObjectType = this.dataObjType.asObservable(); private dataObjTypeParent = new ReplaySubject>(1); currentObjectTypeParent = this.dataObjTypeParent.asObservable(); + private dataBreadCrumb = new ReplaySubject(1); + currentDataBreadCrumb = this.dataBreadCrumb.asObservable(); + constructor() { let storedObjectType = localStorage.getItem('storedObjectType'); let storedObjectTypeParent = localStorage.getItem('storedObjectTypeParent'); - let storedObjectSelected= localStorage.getItem('storedObjectSelected'); - if (storedObjectType) - this.changeObjectType(JSON.parse(storedObjectType), false); + let storedObjectSelected = localStorage.getItem('storedObjectSelected'); + let storedPArentObjectSelected = localStorage.getItem('storedParentObjectSelected'); + let storedDataBreadCrumb = localStorage.getItem('storedDataBreadCrumb'); + if (storedObjectType) this.changeObjectType(JSON.parse(storedObjectType), false); if (storedObjectTypeParent) - this.changeObjectTypeParent(JSON.parse(storedObjectTypeParent), false); - - if (storedObjectSelected) - this.changeSelectedObj(JSON.parse(storedObjectSelected), false); -} + this.changeObjectTypeParent(JSON.parse(storedObjectTypeParent), false); + + if (storedObjectSelected) this.changeSelectedObj(JSON.parse(storedObjectSelected), false); + if (storedPArentObjectSelected) + this.changeSelectedObj(JSON.parse(storedPArentObjectSelected), false); - changeObjectType(newObjType: IobjObs,storeObjectType: boolean = false) { - if (storeObjectType) - localStorage.setItem('storedObjectType', JSON.stringify(newObjType)); - this.dataObjType.next(newObjType); + if (storedDataBreadCrumb) this.changeBreadCrumb(JSON.parse(storedDataBreadCrumb), false); } - changeObjectTypeParent(newObjType: IobjObs,storeObjectTypeParent: boolean = false) { + changeObjectType(newObjType: IobjObs, storeObjectType: boolean = false) { + if (storeObjectType) localStorage.setItem('storedObjectType', JSON.stringify(newObjType)); + this.dataObjType.next(newObjType); + } + + changeObjectTypeParent(newObjType: IobjObs, storeObjectTypeParent: boolean = false) { if (storeObjectTypeParent) localStorage.setItem('storedObjectTypeParent', JSON.stringify(newObjType)); - this.dataObjTypeParent.next(newObjType); + this.dataObjTypeParent.next(newObjType); } - changeSelectedObj(newObjSelected:ObjDataType , storeObjectTypeSelected: boolean = false ){ + changeSelectedObj(newObjSelected: ObjDataType | {}, storeObjectTypeSelected: boolean = false) { if (storeObjectTypeSelected) localStorage.setItem('storedObjectSelected', JSON.stringify(newObjSelected)); - this.objSelected.next(newObjSelected); + this.objSelected.next(newObjSelected); + } + + changeSelectedParentObj( + newObjParentSelected: ObjDataType | {}, + storeParentObjectTypeSelected: boolean = false + ) { + if (storeParentObjectTypeSelected) + localStorage.setItem('storedParentObjectSelected', JSON.stringify(newObjParentSelected)); + this.parentObjSelected.next(newObjParentSelected); + } + + changeBreadCrumb(newDataBreadCrumb: IBreadCrumb[], storeDataBreadCrumb: boolean = false) { + if (storeDataBreadCrumb) + localStorage.setItem('storedDataBreadCrumb', JSON.stringify(newDataBreadCrumb)); + this.dataBreadCrumb.next(newDataBreadCrumb); } } From 3089d927d06d1a69cefae056db4892609b75cbb4 Mon Sep 17 00:00:00 2001 From: Andria Capai Date: Fri, 26 May 2023 18:06:23 +0200 Subject: [PATCH 043/161] feat(wip): merge chainInput and display-data-info Need dev to the two other branches Try to add logic into map-list-component Check if it is the best way to manage map Reviewed-by: andriac --- .../monitoring/objects.py | 2 + .../gn_module_monitoring/routes/monitoring.py | 29 +++ backend/gn_module_monitoring/routes/site.py | 10 +- .../routes/sites_groups.py | 10 +- .../monitoring-form.component-g.html | 17 +- .../monitoring-form.component-g.ts | 186 ++++++++++++------ .../monitoring-map-list.component.css | 6 +- .../monitoring-map-list.component.html | 11 +- .../monitoring-map-list.component.ts | 50 ++++- .../monitoring-sitesgroups.component.ts | 11 +- frontend/app/gnModule.module.ts | 7 +- .../app/resolver/sites-groups.resolver.ts | 30 +++ frontend/app/services/api-geom.service.ts | 20 +- frontend/app/services/config-json.service.ts | 55 +++++- frontend/app/services/form.service.ts | 15 +- 15 files changed, 374 insertions(+), 85 deletions(-) create mode 100644 frontend/app/resolver/sites-groups.resolver.ts diff --git a/backend/gn_module_monitoring/monitoring/objects.py b/backend/gn_module_monitoring/monitoring/objects.py index 002f60113..c1dff173b 100644 --- a/backend/gn_module_monitoring/monitoring/objects.py +++ b/backend/gn_module_monitoring/monitoring/objects.py @@ -27,6 +27,8 @@ class MonitoringSite(MonitoringObjectGeom): def preprocess_data(self, data): if len(data["types_site"]) > 0 and all(isinstance(x, int) for x in data["types_site"]): data["id_nomenclature_type_site"] = data["types_site"][0] + elif "data" in data and data["data"]["id_nomenclature_type_site"]: + data["id_nomenclature_type_site"] = data["data"]["id_nomenclature_type_site"] else: data["id_nomenclature_type_site"] = data["types_site"][0]["id_nomenclature_type_site"] # TODO: A enlever une fois qu'on aura enelever le champ "id_nomenclature_type_site" du model et de la bdd diff --git a/backend/gn_module_monitoring/routes/monitoring.py b/backend/gn_module_monitoring/routes/monitoring.py index d373f167a..5e85c190e 100644 --- a/backend/gn_module_monitoring/routes/monitoring.py +++ b/backend/gn_module_monitoring/routes/monitoring.py @@ -183,6 +183,35 @@ def create_or_update_object_api_sites_sites_group(module_code, object_type, id=N ) +def get_config_object(module_code, object_type, id): + ''' + renvoie un object, à partir de type de l'object et de son id + + :param module_code: reference le module concerne + :param object_type: le type d'object (site, visit, obervation) + :param id : l'identifiant de l'object (de id_base_site pour site) + :type module_code: str + :type object_type: str + :type id: int + + :return: renvoie l'object requis + :rtype: dict + ''' + + # field_name = param.get('field_name') + # value = module_code if object_type == 'module' + get_config(module_code, force=True) + + depth = to_int(request.args.get('depth', 1)) + + return ( + monitoring_definitions + .monitoring_object_instance(module_code, object_type, id) + .get(depth=depth) + # .get(value=value, field_name = field_name) + .serialize(depth) + ) + # update object @blueprint.route("object///", methods=["PATCH"]) @blueprint.route( diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py index 0fd3f7d40..093238ab6 100644 --- a/backend/gn_module_monitoring/routes/site.py +++ b/backend/gn_module_monitoring/routes/site.py @@ -14,7 +14,7 @@ TNomenclatures, ) from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema -from gn_module_monitoring.routes.monitoring import create_or_update_object_api_sites_sites_group +from gn_module_monitoring.routes.monitoring import create_or_update_object_api_sites_sites_group, get_config_object from gn_module_monitoring.utils.routes import ( filter_params, geojson_query, @@ -25,6 +25,14 @@ ) +@blueprint.route("/sites/config", + defaults={'id': None, 'object_type': "site",'module_code':'generic'}, + methods=["GET"]) +def get_config_sites(module_code, object_type, id): + obj = get_config_object(module_code, object_type, id) + return obj['properties'] + + @blueprint.route("/sites/types", methods=["GET"]) def get_types_site(): params = MultiDict(request.args) diff --git a/backend/gn_module_monitoring/routes/sites_groups.py b/backend/gn_module_monitoring/routes/sites_groups.py index 6535b8e9a..0da117a75 100644 --- a/backend/gn_module_monitoring/routes/sites_groups.py +++ b/backend/gn_module_monitoring/routes/sites_groups.py @@ -19,10 +19,18 @@ paginate, sort, ) -from gn_module_monitoring.routes.monitoring import create_or_update_object_api_sites_sites_group +from gn_module_monitoring.routes.monitoring import create_or_update_object_api_sites_sites_group, get_config_object from gn_module_monitoring.utils.utils import to_int +@blueprint.route("/sites_groups/config", + defaults={'id': None, 'object_type': "sites_group",'module_code':'generic'}, + methods=["GET"]) +def get_config_sites_groups(module_code, object_type, id): + obj = get_config_object(module_code, object_type, id) + return obj['properties'] + + @blueprint.route("/sites_groups", methods=["GET"]) def get_sites_groups(): params = MultiDict(request.args) diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html index 27b66a108..fff8e04f6 100644 --- a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html @@ -18,12 +18,12 @@

Attention

-
+
- + @@ -94,14 +94,15 @@

Attention

aria-hidden="true" > - - - Valider + > + Valider + +
+
+ + + + + +
+ {{ fields[fieldName] }} + + > + {{ selectedObj[fieldName] }}
+
- > + >help --> + {{ selectedObj[fieldName] }} diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts index 32336f5cd..70352acad 100644 --- a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts @@ -30,6 +30,10 @@ export class MonitoringPropertiesGComponent implements OnInit { datasetForm = new FormControl(); _sub: Subscription; + specificFields: JsonData; + specificFieldDefinitions: JsonData = {}; + specificFieldsNames: string[]; + constructor(private _formService: FormService, private _objService: ObjectService) {} ngOnInit() { @@ -44,6 +48,12 @@ export class MonitoringPropertiesGComponent implements OnInit { this.endPoint = this.newParentType.endPoint; } + initSpecificProperties(){ + this.specificFieldsNames = this.newParentType.template_specific.fieldNames; + this.specificFields = this.newParentType.template_specific.fieldLabels; + this.specificFieldDefinitions = this.newParentType.template_specific.fieldDefinitions; + } + onEditClick() { this.selectedObj['id'] = this.selectedObj[this.selectedObj.pk]; this._formService.changeDataSub( @@ -57,6 +67,9 @@ export class MonitoringPropertiesGComponent implements OnInit { ngOnChanges(changes: SimpleChanges): void { if (this.newParentType && this.newParentType.template.fieldNames.length != 0) { this.initProperties(); + if (this.newParentType.template_specific && this.newParentType.template_specific.fieldNames.length !=0){ + this.initSpecificProperties() + } } } } diff --git a/frontend/app/components/monitoring-visits/monitoring-visits.component.ts b/frontend/app/components/monitoring-visits/monitoring-visits.component.ts index b66c1203e..1ce289e41 100644 --- a/frontend/app/components/monitoring-visits/monitoring-visits.component.ts +++ b/frontend/app/components/monitoring-visits/monitoring-visits.component.ts @@ -17,6 +17,7 @@ import { Module } from '../../interfaces/module'; import { ConfigService } from '../../services/config.service'; import { FormService } from "../../services/form.service"; import { breadCrumbElementBase } from '../breadcrumbs/breadcrumbs.component'; +import { ConfigJsonService } from '../../services/config-json.service'; @Component({ selector: 'monitoring-visits', templateUrl: './monitoring-visits.component.html', @@ -59,7 +60,8 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement private _formBuilder: FormBuilder, private _formService: FormService, private _configService: ConfigService, - public siteService: SitesService + public siteService: SitesService, + protected _configJsonService: ConfigJsonService ) { super(); this.getAllItemsCallback = this.getVisits; @@ -96,6 +98,13 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement }), }).pipe(map((data)=> {return data})) }), + mergeMap((data)=> { + return this.siteService.getTypesSiteByIdSite(data.site.id_base_site).pipe( + map((types_site:any) => { + return {site:data.site, visits: data.visits, types_site:types_site} + } + )) + }), mergeMap((data)=>{ return forkJoin({ objObsSite: this.siteService.initConfig(), @@ -108,7 +117,7 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement return this._objService.currentObjSelected.pipe( take(1), map((objSelectParent:any) => { - return { site: data.site, visits: data.visits, parentObjSelected: objSelectParent, objConfig:objConfig }; + return {types_site:data.types_site, site: data.site, visits: data.visits, parentObjSelected: objSelectParent, objConfig:objConfig }; }) ); }), @@ -118,7 +127,7 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement of(data), this._sitesGroupService.getById(this.siteGroupIdParent).pipe( map((objSelectParent) => { - return { site: data.site, visits: data.visits, parentObjSelected: objSelectParent, objConfig:data.objConfig }; + return {types_site:data.types_site, site: data.site, visits: data.visits, parentObjSelected: objSelectParent, objConfig:data.objConfig }; }) ) ); @@ -211,7 +220,29 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement } addSpecificConfig(){ - this.objParent["template_specific"] + // const schemaSpecificType = Object.assign({},...this.types_site) + let schemaSpecificType = {} + let schemaTypeMerged = {} + for (let type_site of this.types_site){ + if('specific' in type_site['config']) { + Object.assign(schemaSpecificType, type_site['config']['specific']) + Object.assign(schemaTypeMerged,type_site['config'] ) + } + } + + + const fieldNames = this._configJsonService.fieldNames('generic','site','display_properties',schemaTypeMerged) + const fieldNamesList = this._configJsonService.fieldNames('generic','site','display_list',schemaTypeMerged) + const fieldLabels = this._configJsonService.fieldLabels(schemaSpecificType); + const fieldDefinitions = this._configJsonService.fieldDefinitions(schemaSpecificType); + this.objParent["template_specific"] ={} + this.objParent["template_specific"]['fieldNames'] = fieldNames; + this.objParent["template_specific"]['fieldNamesList'] = fieldNamesList; + this.objParent["template_specific"]['schema']= schemaSpecificType; + this.objParent["template_specific"]['fieldLabels'] = fieldLabels; + this.objParent["template_specific"]['fieldDefinitions'] = fieldDefinitions; + this.objParent["template_specific"]['fieldNamesList'] = fieldNamesList; + } initValueToSend() { diff --git a/frontend/app/services/api-geom.service.ts b/frontend/app/services/api-geom.service.ts index f4c7f19be..a7c6a22d9 100644 --- a/frontend/app/services/api-geom.service.ts +++ b/frontend/app/services/api-geom.service.ts @@ -192,6 +192,10 @@ export class SitesService extends ApiGeomService { ); } + getTypesSiteByIdSite(idSite:number):Observable{ + return this._cacheService.request>('get',`/sites/${idSite}/types`) + } + getSiteModules(idSite: number): Observable { return this._cacheService.request('get', `sites/${idSite}/modules`); } diff --git a/frontend/app/services/config-json.service.ts b/frontend/app/services/config-json.service.ts index 7148fb3df..aa30d5613 100644 --- a/frontend/app/services/config-json.service.ts +++ b/frontend/app/services/config-json.service.ts @@ -45,8 +45,11 @@ export class ConfigJsonService extends ConfigService { return fieldLabels; } - fieldNames(moduleCode, objectType, typeDisplay = '') { + fieldNames(moduleCode, objectType, typeDisplay = '',confObject = {}) { if (['display_properties', 'display_list'].includes(typeDisplay)) { + if (Object.keys(confObject).length > 0){ + return confObject[typeDisplay] + } return this.configModuleObjectParam(moduleCode, objectType, typeDisplay); } if (typeDisplay === 'schema') { From 18fb31af19b1f6328efc1001b41261e6cd2beef7 Mon Sep 17 00:00:00 2001 From: Andria Capai Date: Tue, 11 Jul 2023 17:47:27 +0200 Subject: [PATCH 071/161] feat: display specific properties Display all value from specific config even if it's null Reviewed-by: andriacap --- .../monitoring-properties-g.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html index 87eb7cbfc..6ca2dd6d1 100644 --- a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html @@ -74,9 +74,9 @@ attr.aria-labelledby="nav-specific-tab" > - + - +
- {{ specificFields[fieldName] }} + {{ fieldName.value }} {{ selectedObj[fieldName] }}{{ selectedObj.data[fieldName.key] }}
From dc7f28d1f0b85500b010df3cf06d4b73b429a832 Mon Sep 17 00:00:00 2001 From: Andria Capai Date: Tue, 11 Jul 2023 17:49:44 +0200 Subject: [PATCH 072/161] fix: remove call api for types_site Useless call api in visit-component (already get by another request) Remove '/' from start of request to avoid double '//' Reviewed-by: andriacap --- .../monitoring-visits/monitoring-visits.component.ts | 11 ++--------- frontend/app/services/api-geom.service.ts | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/frontend/app/components/monitoring-visits/monitoring-visits.component.ts b/frontend/app/components/monitoring-visits/monitoring-visits.component.ts index 1ce289e41..faf074b5d 100644 --- a/frontend/app/components/monitoring-visits/monitoring-visits.component.ts +++ b/frontend/app/components/monitoring-visits/monitoring-visits.component.ts @@ -98,13 +98,6 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement }), }).pipe(map((data)=> {return data})) }), - mergeMap((data)=> { - return this.siteService.getTypesSiteByIdSite(data.site.id_base_site).pipe( - map((types_site:any) => { - return {site:data.site, visits: data.visits, types_site:types_site} - } - )) - }), mergeMap((data)=>{ return forkJoin({ objObsSite: this.siteService.initConfig(), @@ -117,7 +110,7 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement return this._objService.currentObjSelected.pipe( take(1), map((objSelectParent:any) => { - return {types_site:data.types_site, site: data.site, visits: data.visits, parentObjSelected: objSelectParent, objConfig:objConfig }; + return {site: data.site, visits: data.visits, parentObjSelected: objSelectParent, objConfig:objConfig }; }) ); }), @@ -127,7 +120,7 @@ export class MonitoringVisitsComponent extends MonitoringGeomComponent implement of(data), this._sitesGroupService.getById(this.siteGroupIdParent).pipe( map((objSelectParent) => { - return {types_site:data.types_site, site: data.site, visits: data.visits, parentObjSelected: objSelectParent, objConfig:data.objConfig }; + return {site: data.site, visits: data.visits, parentObjSelected: objSelectParent, objConfig:data.objConfig }; }) ) ); diff --git a/frontend/app/services/api-geom.service.ts b/frontend/app/services/api-geom.service.ts index a7c6a22d9..3ac167f23 100644 --- a/frontend/app/services/api-geom.service.ts +++ b/frontend/app/services/api-geom.service.ts @@ -193,7 +193,7 @@ export class SitesService extends ApiGeomService { } getTypesSiteByIdSite(idSite:number):Observable{ - return this._cacheService.request>('get',`/sites/${idSite}/types`) + return this._cacheService.request>('get',`sites/${idSite}/types`) } getSiteModules(idSite: number): Observable { From cf8b94dc6412d97a0a2e92439c30c9824f0d15e8 Mon Sep 17 00:00:00 2001 From: Andria Capai Date: Tue, 18 Jul 2023 10:41:54 +0200 Subject: [PATCH 073/161] feat(back): json formatter on edit column Add class JSON to format json in edit column [Refs_ticket]: Closes #75 Reviewed-by: andriacap --- backend/gn_module_monitoring/monitoring/admin.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/gn_module_monitoring/monitoring/admin.py b/backend/gn_module_monitoring/monitoring/admin.py index 0fcdc14df..744b2faa7 100644 --- a/backend/gn_module_monitoring/monitoring/admin.py +++ b/backend/gn_module_monitoring/monitoring/admin.py @@ -1,4 +1,6 @@ +from flask import json from flask_admin.contrib.sqla import ModelView +from flask_admin.form import fields from geonature.core.admin.admin import CruvedProtectedMixin from geonature.utils.env import DB from pypnnomenclature.models import BibNomenclaturesTypes, TNomenclatures @@ -30,6 +32,16 @@ def __call__(self, form, field): raise ValidationError(self.message) +class JSONField(fields.JSONField): + def _value(self): + if self.raw_data: + return self.raw_data[0] + elif self.data: + return json.dumps(self.data, ensure_ascii=False, indent=2) + else: + return "" + + class BibTypeSiteView(CruvedProtectedMixin, ModelView): """ Surcharge de l'administration des types de sites @@ -70,7 +82,7 @@ def list_label_nomenclature_formatter(view, _context, model, _name): validators=[Unique(BibTypeSite, "id_nomenclature_type_site", "id_nomenclature")], ) ) - + form_overrides = {"config": JSONField} column_list = ("nomenclature", "config") column_formatters = dict(nomenclature=list_label_nomenclature_formatter, config=json_formatter) form_excluded_columns = "sites" From 6fd166b47bb355330a608808d6c3f231d3312b34 Mon Sep 17 00:00:00 2001 From: Andria Capai Date: Tue, 18 Jul 2023 15:23:32 +0200 Subject: [PATCH 074/161] feat(back): edition site work for module enter Change the "types_site" properties by id_nomenclature (before label_fr) just before post/patch data in "preprocess_data" WIP: see if it's possible to add different "types_site" ? Currently the way is to change type_site only when you enter by site Reviewed-by: andriacap --- .../monitoring/objects.py | 29 ++++++++++++---- .../monitoring/serializer.py | 34 ++++++++----------- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/backend/gn_module_monitoring/monitoring/objects.py b/backend/gn_module_monitoring/monitoring/objects.py index c1dff173b..16eaed0a7 100644 --- a/backend/gn_module_monitoring/monitoring/objects.py +++ b/backend/gn_module_monitoring/monitoring/objects.py @@ -3,6 +3,7 @@ from geonature.utils.env import DB from geonature.core.gn_commons.models import TModules + class MonitoringModule(MonitoringObject): def get(self, param_value=None, param_name=None, depth=0): """ @@ -24,11 +25,27 @@ class MonitoringSite(MonitoringObjectGeom): avec la méthode from_dict """ - def preprocess_data(self, data): - if len(data["types_site"]) > 0 and all(isinstance(x, int) for x in data["types_site"]): - data["id_nomenclature_type_site"] = data["types_site"][0] - elif "data" in data and data["data"]["id_nomenclature_type_site"]: - data["id_nomenclature_type_site"] = data["data"]["id_nomenclature_type_site"] + def preprocess_data(self, properties, data=[]): + if len(data) != 0: + if len(data["types_site"]) > 0 and all(isinstance(x, int) for x in data["types_site"]): + properties["id_nomenclature_type_site"] = data["types_site"][0] + properties["types_site"] = data["types_site"] + + elif "data" in data and data["data"]["id_nomenclature_type_site"]: + properties["id_nomenclature_type_site"] = data["data"]["id_nomenclature_type_site"] + else: + properties["id_nomenclature_type_site"] = data["types_site"][0][ + "id_nomenclature_type_site" + ] else: - data["id_nomenclature_type_site"] = data["types_site"][0]["id_nomenclature_type_site"] + if len(properties.get("types_site", [])) != 0: + if hasattr(self._model, "types_site"): + properties["types_site"] = [] + # TODO: performance? + # for type in properties['types_site']: + # properties['types_site'].append(types_site) + types_site = [ + typ.nomenclature.id_nomenclature for typ in self._model.types_site + ] + properties["types_site"] = types_site # TODO: A enlever une fois qu'on aura enelever le champ "id_nomenclature_type_site" du model et de la bdd diff --git a/backend/gn_module_monitoring/monitoring/serializer.py b/backend/gn_module_monitoring/monitoring/serializer.py index 47288871f..cbd48634f 100644 --- a/backend/gn_module_monitoring/monitoring/serializer.py +++ b/backend/gn_module_monitoring/monitoring/serializer.py @@ -12,23 +12,15 @@ class MonitoringObjectSerializer(MonitoringObjectBase): - - def get_parent(self): parent_type = self.parent_type() if not parent_type: return if not self._parent: - self._parent = ( - monitoring_definitions - .monitoring_object_instance( - self._module_code, - parent_type, - self.id_parent() - ) - .get() - ) + self._parent = monitoring_definitions.monitoring_object_instance( + self._module_code, parent_type, self.id_parent() + ).get() return self._parent @@ -57,8 +49,8 @@ def flatten_specific_properties(self, properties): def unflatten_specific_properties(self, properties): data = {} - for attribut_name in self.config_schema('specific'): - if attribut_name != 'html': + for attribut_name, attribut_value in self.config_schema("specific").items(): + if "type_widget" in attribut_value and attribut_value["type_widget"] != "html": val = properties.pop(attribut_name) data[attribut_name] = val @@ -66,7 +58,7 @@ def unflatten_specific_properties(self, properties): properties["data"] = data def serialize_children(self, depth): - children_types = self.config_param('children_types') + children_types = self.config_param("children_types") if not children_types: return @@ -83,9 +75,8 @@ def serialize_children(self, depth): children_of_type = [] for child_model in getattr(self._model, relation_name): - child = ( - monitoring_definitions - .monitoring_object_instance(self._module_code, children_type, model=child_model) + child = monitoring_definitions.monitoring_object_instance( + self._module_code, children_type, model=child_model ) children_of_type.append(child.serialize(depth)) @@ -176,12 +167,15 @@ def populate(self, post_data): self.unflatten_specific_properties(properties) # pretraitement (pour t_base_site et cor_site_module) - self.preprocess_data(properties) + if "dataComplement" in post_data: + self.preprocess_data(properties, post_data["dataComplement"]) + else: + self.preprocess_data(properties) # ajout des données en base - if hasattr(self._model, 'from_geofeature'): + if hasattr(self._model, "from_geofeature"): for key in list(post_data): - if key not in ("properties","geometry","type"): + if key not in ("properties", "geometry", "type"): post_data.pop(key) self._model.from_geofeature(post_data, True) else: From 0aba4efb5e1ec30df322e7dfe632b707247d44f0 Mon Sep 17 00:00:00 2001 From: Andria Capai Date: Tue, 18 Jul 2023 15:27:34 +0200 Subject: [PATCH 075/161] refact: remove useless call service in btn-select the service to add Extra formController to formGroup was call too many time for nothing. Reviewed-by: andriacap --- frontend/app/components/btn-select/btn-select.component.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/app/components/btn-select/btn-select.component.ts b/frontend/app/components/btn-select/btn-select.component.ts index e6df25e6a..83531cdef 100644 --- a/frontend/app/components/btn-select/btn-select.component.ts +++ b/frontend/app/components/btn-select/btn-select.component.ts @@ -92,7 +92,6 @@ export class BtnSelectComponent implements OnInit { } this.sendobject.emit(this.configObjAdded); this.listOpNeeded.setValue(this.listOptionChosen) - this._formService.changeExtraFormControl(this.listOpNeeded,"listOptBtnSelect") } selected(event: MatAutocompleteSelectedEvent): void { @@ -103,7 +102,6 @@ export class BtnSelectComponent implements OnInit { this.optionInput.nativeElement.value = ''; this.myControl.setValue(null); this.listOpNeeded.setValue(this.listOptionChosen) - this._formService.changeExtraFormControl(this.listOpNeeded,"listOptBtnSelect") } filterOnRequest(val: string, keyToFilt: string): Observable { From b6ba0405ee5a62f82d264fcd88d2abcdc18560d5 Mon Sep 17 00:00:00 2001 From: Andria Capai Date: Tue, 18 Jul 2023 15:30:03 +0200 Subject: [PATCH 076/161] fix: error on tab spec properties When "selectedObj.data" is empty we do not display tab and table (adding this part of code to remove the error) Reviewed-by: andriacap --- .../monitoring-properties-g.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html index 6ca2dd6d1..dda08147c 100644 --- a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html @@ -68,6 +68,7 @@
Date: Tue, 18 Jul 2023 15:32:57 +0200 Subject: [PATCH 077/161] feat(front): architecture form with dynamic field Change the way to place the component "btn-select" into the form-component-g. Also add another dynamic form generator in order to update separetely all specific fields and static fields Reviewed-by: andriacap --- .../monitoring-form.component-g.html | 32 +++++++++++++------ .../monitoring-sites-create.component.html | 17 +++++----- .../monitoring-visits.component.html | 27 ++++++++-------- 3 files changed, 46 insertions(+), 30 deletions(-) diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html index 73541fb7c..3da2adc62 100644 --- a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html @@ -19,7 +19,7 @@

Attention

-
+
@@ -40,12 +40,13 @@

Attention

- + +

Veuillez saisir une géométrie sur la carte

@@ -53,15 +54,28 @@

Attention

- + + + + +
@@ -86,7 +100,7 @@

Attention