From 5cfd2846b17368668e2439b00765d223f8717e1a Mon Sep 17 00:00:00 2001 From: Maxime Vergez Date: Mon, 2 Oct 2023 11:58:51 +0200 Subject: [PATCH 01/12] feat(db): add migration for individuals refactor(db): move base individuals tables to GN feat(db): add id_individual col to observations fix(db): remove permission before removing objects fix(db): update individuals => cd_nom When dropping id_individual column. To keep observations data --- .../0790c7d024fb_add_individuals.py | 61 +++++++++++++++++ ...66_add_id_individual_col_t_observations.py | 65 +++++++++++++++++++ ...461b82ee737a_add_individual_permissions.py | 46 +++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 backend/gn_module_monitoring/migrations/0790c7d024fb_add_individuals.py create mode 100644 backend/gn_module_monitoring/migrations/2894b3c03c66_add_id_individual_col_t_observations.py create mode 100644 backend/gn_module_monitoring/migrations/461b82ee737a_add_individual_permissions.py diff --git a/backend/gn_module_monitoring/migrations/0790c7d024fb_add_individuals.py b/backend/gn_module_monitoring/migrations/0790c7d024fb_add_individuals.py new file mode 100644 index 000000000..ad8c573b3 --- /dev/null +++ b/backend/gn_module_monitoring/migrations/0790c7d024fb_add_individuals.py @@ -0,0 +1,61 @@ +"""add individuals + +Revision ID: 0790c7d024fb +Revises: fc90d31c677f +Create Date: 2023-09-27 14:01:26.035798 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import JSONB + + +# revision identifiers, used by Alembic. +revision = "0790c7d024fb" +down_revision = "fc90d31c677f" +branch_labels = None +depends_on = "84f40d008640" # individuals (geonature) + +SCHEMA = "gn_monitoring" + + +def upgrade(): + op.create_table( + "t_individual_complements", + sa.Column( + "id_base_individual", + sa.Integer, + sa.ForeignKey(f"{SCHEMA}.t_base_individuals.id_base_individual", ondelete="CASCADE"), + primary_key=True, + ), + sa.Column( + "id_module", + sa.Integer, + sa.ForeignKey("gn_commons.t_modules.id_module", ondelete="CASCADE"), + primary_key=True, + ), + sa.Column("data", JSONB), + schema=SCHEMA, + ) + + op.create_table( + "cor_individual_module", + sa.Column( + "id_base_individual", + sa.Integer, + sa.ForeignKey(f"{SCHEMA}.t_base_individuals.id_base_individual", ondelete="CASCADE"), + primary_key=True, + ), + sa.Column( + "id_module", + sa.Integer, + sa.ForeignKey("gn_commons.t_modules.id_module", ondelete="CASCADE"), + primary_key=True, + ), + schema=SCHEMA, + ) + + +def downgrade(): + op.drop_table("cor_individual_module", schema=SCHEMA) + op.drop_table("t_individual_complements", schema=SCHEMA) diff --git a/backend/gn_module_monitoring/migrations/2894b3c03c66_add_id_individual_col_t_observations.py b/backend/gn_module_monitoring/migrations/2894b3c03c66_add_id_individual_col_t_observations.py new file mode 100644 index 000000000..31d754327 --- /dev/null +++ b/backend/gn_module_monitoring/migrations/2894b3c03c66_add_id_individual_col_t_observations.py @@ -0,0 +1,65 @@ +"""add id_individual col t_observations + +Revision ID: 2894b3c03c66 +Revises: fc90d31c677f +Create Date: 2023-11-21 11:06:04.284038 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.sql import column + + +# revision identifiers, used by Alembic. +revision = "2894b3c03c66" +down_revision = "fc90d31c677f" +branch_labels = None +depends_on = "84f40d008640" # t_individuals (geonature) + +monitorings_schema = "gn_monitoring" +table = "t_observations" +column_name = "id_individual" +foreign_schema = monitorings_schema +foreign_table = "t_individuals" +foreign_key = column_name + +constraint_name = f"check_{table}_cd_nom_or_id_individual_not_null" +cd_nom_column_name = "cd_nom" + + +def upgrade(): + op.add_column( + table, + sa.Column( + column_name, + sa.Integer(), + sa.ForeignKey( + f"{foreign_schema}.{foreign_table}.{foreign_key}", + name=f"fk_{table}_{column_name}", + onupdate="CASCADE", + ), + ), + schema=monitorings_schema, + ) + op.alter_column( + table_name=table, column_name=cd_nom_column_name, nullable=True, schema=monitorings_schema + ) + op.create_check_constraint( + table_name=table, + constraint_name=constraint_name, + condition=sa.or_(column(cd_nom_column_name).isnot(None), column(column_name).isnot(None)), + schema=monitorings_schema, + ) + + +def downgrade(): + op.execute(""" + UPDATE gn_monitoring.t_observations SET cd_nom = ind.cd_nom + FROM gn_monitoring.t_individuals ind + WHERE ind.id_individual = gn_monitoring.t_observations.id_individual; + """) + op.drop_column(table_name=table, column_name=column_name, schema=monitorings_schema) + op.alter_column( + table_name=table, column_name=cd_nom_column_name, nullable=False, schema=monitorings_schema + ) + # constraint automatically dropped with drop_column above diff --git a/backend/gn_module_monitoring/migrations/461b82ee737a_add_individual_permissions.py b/backend/gn_module_monitoring/migrations/461b82ee737a_add_individual_permissions.py new file mode 100644 index 000000000..f1409a4ed --- /dev/null +++ b/backend/gn_module_monitoring/migrations/461b82ee737a_add_individual_permissions.py @@ -0,0 +1,46 @@ +"""add individual permissions + +Revision ID: 461b82ee737a +Revises: 2894b3c03c66 +Create Date: 2023-11-21 14:14:48.084725 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '461b82ee737a' +down_revision = '2894b3c03c66' +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute( + """ + INSERT INTO gn_permissions.t_objects (code_object, description_object) + VALUES ('MONITORINGS_INDIVIDUALS', 'Permissions sur les individus'), + ('MONITORINGS_MARKINGS', 'Permissions sur les marquages'); + """ + ) + + +def downgrade(): + op.execute( + """ + DELETE FROM gn_permissions.t_permissions WHERE id_object in + (SELECT id_object FROM gn_permissions.t_objects WHERE code_object in ('MONITORINGS_INDIVIDUALS', 'MONITORINGS_MARKINGS')) + """ + ) + op.execute( + """ + DELETE FROM gn_permissions.t_permissions_available WHERE id_object in + (SELECT id_object FROM gn_permissions.t_objects WHERE code_object in ('MONITORINGS_INDIVIDUALS', 'MONITORINGS_MARKINGS')) + """ + ) + op.execute( + """ + DELETE FROM gn_permissions.t_objects where code_object in ('MONITORINGS_INDIVIDUALS', 'MONITORINGS_MARKINGS'); + """ + ) From 6a589786f991eb95b0f0b72dd80b81b496c2b8bc Mon Sep 17 00:00:00 2001 From: Maxime Vergez Date: Wed, 4 Oct 2023 11:06:08 +0200 Subject: [PATCH 02/12] feat(api): add orm models feat(api): add definition, linking to orm models feat(api): add more attributes to orm models refactor(api): move everything to GeoNature feat(api): specific model dict for individuals feat(config): add first individual json file feat(api): more config to individuals.json refactor: update config following changes in model feat(api): add soft delete refactor(api): add marking as a individual child refactor(config): mv marking props to marking.json From individual.json feat(api): add nb_sites for individuals feat(api): add nb_individuals to site feat: add individuals to site.json feat(api): add id_individual column feat(api): add id_individual in observation.json Otherwise it goes into data feat(api): add individual permissions feat(config): add id_digitiser to marking feat(api): add cd_nom parameter for the module Add a global variable __MODULE.CD_NOM, stored in the t_module_complements table. It enables the user to provide the CD_NOM several times in the json config files like __MODULE.ID_LIST_TAXONOMY for instance... feat(api): remove relationship Since it has been moved to GeoNature chore(api): fix sqlalchemy warning ViewOnly & scalar_subquery() feat(config): hide cd_nom by default feat(api): really delete object no deactivation style(api): apply black fix: disable viewOnl=True to remove warning fix: reorder revisions to make it ok for clean install fix: cd_nom missing on post observation Reviewed-by: andriacap fix: change revises migration to the good one Reviewed-by: andriac fix: replace revises by the good one and rm pass Change revises to match with the one used Remove useless "pass" Reviewed-by: andriac fix: nb_individus and nb_sites for Tindividuals problem with query to find number of site and individuals Reviewed-by: andriacap fix: add distinct on nb_individuals for site Reviewed-by: andriacap refactor: add nb_individuals outside models Add nb_inidivuals property as before but write notes to understand why it was needed ? It seems to work without .. (tried) Reviewed-by: andriacap --- backend/gn_module_monitoring/command/utils.py | 2 + .../gn_module_monitoring/conf_schema_toml.py | 2 + .../config/generic/config.json | 1 + .../config/generic/individual.json | 86 ++++++++++++++++++ .../config/generic/marking.json | 89 +++++++++++++++++++ .../config/generic/module.json | 10 ++- .../config/generic/observation.json | 12 ++- .../config/generic/site.json | 14 ++- .../config/repositories.py | 1 + .../0790c7d024fb_add_individuals.py | 61 ------------- ...66_add_id_individual_col_t_observations.py | 11 ++- ...4b364f7_add_cd_nom_t_module_complements.py | 36 ++++++++ ...461b82ee737a_add_individual_permissions.py | 7 +- ...528c94d350_upgrade_existing_permissions.py | 4 +- .../monitoring/definitions.py | 12 ++- .../gn_module_monitoring/monitoring/models.py | 63 +++++++++++-- .../monitoring/objects.py | 21 +++++ .../monitoring/repositories.py | 3 + .../monitoring/serializer.py | 23 ++++- 19 files changed, 372 insertions(+), 86 deletions(-) create mode 100644 backend/gn_module_monitoring/config/generic/individual.json create mode 100644 backend/gn_module_monitoring/config/generic/marking.json delete mode 100644 backend/gn_module_monitoring/migrations/0790c7d024fb_add_individuals.py create mode 100644 backend/gn_module_monitoring/migrations/398f94b364f7_add_cd_nom_t_module_complements.py diff --git a/backend/gn_module_monitoring/command/utils.py b/backend/gn_module_monitoring/command/utils.py index 4d1c261a1..137435999 100644 --- a/backend/gn_module_monitoring/command/utils.py +++ b/backend/gn_module_monitoring/command/utils.py @@ -55,6 +55,8 @@ "MONITORINGS_GRP_SITES": {"label": "groupes de sites", "actions": ["C", "R", "U", "D"]}, "MONITORINGS_SITES": {"label": "sites", "actions": ["C", "R", "U", "D"]}, "MONITORINGS_VISITES": {"label": "visites", "actions": ["C", "R", "U", "D"]}, + "MONITORINGS_INDIVIDUALS": {"label": "individus", "actions": ["C", "R", "U", "D"]}, + "MONITORINGS_MARKINGS": {"label": "marquages", "actions": ["C", "R", "U", "D"]}, } ACTION_LABEL = { diff --git a/backend/gn_module_monitoring/conf_schema_toml.py b/backend/gn_module_monitoring/conf_schema_toml.py index bdc2c10b2..5d14c60b9 100644 --- a/backend/gn_module_monitoring/conf_schema_toml.py +++ b/backend/gn_module_monitoring/conf_schema_toml.py @@ -15,6 +15,8 @@ "visit": "MONITORINGS_VISITES", "observation": "MONITORINGS_VISITES", "observation_detail": "MONITORINGS_VISITES", + "individual": "MONITORINGS_INDIVIDUALS", + "marking": "MONITORINGS_MARKINGS", } diff --git a/backend/gn_module_monitoring/config/generic/config.json b/backend/gn_module_monitoring/config/generic/config.json index 76365477f..8fa736b3b 100644 --- a/backend/gn_module_monitoring/config/generic/config.json +++ b/backend/gn_module_monitoring/config/generic/config.json @@ -18,6 +18,7 @@ "observer_list": "nom_liste", "taxonomy": "__MODULE.TAXONOMY_DISPLAY_FIELD_NAME", "taxonomy_list": "nom_liste", + "cd_nom": "cd_nom", "sites_group": "sites_group_name", "habitat": "lb_hab_fr", "area": "area_name", diff --git a/backend/gn_module_monitoring/config/generic/individual.json b/backend/gn_module_monitoring/config/generic/individual.json new file mode 100644 index 000000000..8c07192fa --- /dev/null +++ b/backend/gn_module_monitoring/config/generic/individual.json @@ -0,0 +1,86 @@ +{ + "cruved": {"C":1, "U":1, "D": 1}, + "id_field_name": "id_individual", + "description_field_name": "individual_name", + "chained": true, + "filters": { + "active": true + }, + "label": "Individu", + "genre": "M", + "display_properties": ["uuid_individual", + "individual_name", + "active", + "id_nomenclature_sex", + "comment", + "cd_nom", + "id_digitiser", + "id_operator" + ], + "display_list": [ + "individual_name", + "uuid_individual", + "active", + "nb_sites" + ], + "uuid_field_name": "uuid_individual", + "generic": { + "id_individual": { + "type_widget": "text", + "attribut_label": "Id individual", + "hidden": true + }, + "nb_sites": { + "attribut_label": "Nombre de sites associés" + }, + "uuid_individual": { + "attribut_label": "uuid" + }, + "individual_name": { + "type_widget": "text", + "attribut_label": "Nom de l'individu", + "required": true + }, + "id_nomenclature_sex": { + "type_widget": "nomenclature", + "attribut_label": "Sexe", + "code_nomenclature_type": "SEXE", + "type_util": "nomenclature" + }, + "active": { + "type_widget": "bool_checkbox", + "attribut_label": "Actif", + "definition": "Activer cet individu", + "value": true + }, + "comment": { + "type_widget": "textarea", + "attribut_label": "Commentaires" + }, + "cd_nom": { + "type_widget": "taxonomy", + "attribut_label": "Espèce", + "type_util": "taxonomy", + "id_list": "__MODULE.ID_LIST_TAXONOMY", + "required": "({value}) => '__MODULE.CD_NOM' === 'None'", + "hidden": "({value}) => '__MODULE.CD_NOM' !== 'None'" + }, + "id_digitiser": { + "type_widget": "text", + "attribut_label": "Numérisateur", + "required": true, + "hidden": true, + "type_util": "user" + } + }, + "change": [ + "({objForm, meta}) => {", + "const cd_nom = __MODULE.CD_NOM", + "if (!objForm.controls.cd_nom.dirty) {", + "objForm.patchValue({cd_nom: cd_nom})", + "}", + "}", + "" + ] +} + \ No newline at end of file diff --git a/backend/gn_module_monitoring/config/generic/marking.json b/backend/gn_module_monitoring/config/generic/marking.json new file mode 100644 index 000000000..8dfa63f15 --- /dev/null +++ b/backend/gn_module_monitoring/config/generic/marking.json @@ -0,0 +1,89 @@ +{ + "cruved": { + "C": 1, + "U": 1, + "D": 1 + }, + "id_field_name": "id_marking", + "description_field_name": "id_marking", + "chained": true, + "label": "Marquage", + "genre": "M", + "display_properties": [ + "marking_location", + "marking_code", + "marking_details" + ], + "display_list": [ + "marking_location", + "marking_code", + "marking_details", + "id_nomenclature_marking_type", + "id_base_marking_site", + "id_operator" + ], + "uuid_field_name": "id_marking", + "generic": { + "id_marking": { + "type_widget": "text", + "attribut_label": "Id marquage", + "hidden": true + }, + "id_individual": { + "type_widget": "individuals", + "attribut_label": "Choix de l'individu", + "id_module": "__MODULE.ID_MODULE", + "hidden": true + }, + "marking_date": { + "type_widget": "date", + "attribut_label": "Date de marquage", + "required": true + }, + "id_operator": { + "type_widget": "datalist", + "attribut_label": "Opérateur", + "api": "users/menu/__MODULE.ID_LIST_OBSERVER", + "application": "GeoNature", + "keyValue": "id_role", + "keyLabel": "nom_complet", + "type_util": "user", + "required": true + }, + "id_base_marking_site": { + "type_widget": "datalist", + "attribut_label": "Site", + "type_util": "site", + "keyValue": "id_base_site", + "keyLabel": "base_site_name", + "api": "__MONITORINGS_PATH/list/__MODULE.MODULE_CODE/site?id_module=__MODULE.ID_MODULE&fields=id_base_site&fields=base_site_name", + "application": "GeoNature" + }, + "id_nomenclature_marking_type": { + "type_widget": "nomenclature", + "attribut_label": "Type de marquage", + "code_nomenclature_type": "TYP_MARQUAGE", + "type_util": "nomenclature", + "required": true + }, + "marking_location": { + "type_widget": "text", + "attribut_label": "Localisation du marquage" + }, + "marking_code": { + "type_widget": "text", + "attribut_label": "Code du marquage" + }, + "marking_details": { + "type_widget": "text", + "attribut_label": "Détails du marquage" + }, + "id_digitiser": { + "type_widget": "text", + "attribut_label": "Numérisateur", + "required": true, + "hidden": true, + "type_util": "user" + } + } +} \ No newline at end of file diff --git a/backend/gn_module_monitoring/config/generic/module.json b/backend/gn_module_monitoring/config/generic/module.json index 64875e008..a36229ed0 100644 --- a/backend/gn_module_monitoring/config/generic/module.json +++ b/backend/gn_module_monitoring/config/generic/module.json @@ -99,8 +99,6 @@ "definition": "Affichage des groupes de site en dessinant l'enveloppe des sites du groupe et en affichant l'aire du groupe de sites", "hidden": true }, - - "taxonomy_display_field_name": { "type_widget": "datalist", "attribut_label": "Affichage des taxons", @@ -116,7 +114,13 @@ "required": true, "designStyle": "bootstrap" }, - + "cd_nom": { + "type_widget": "taxonomy", + "attribut_label": "Espèce", + "type_util": "taxonomy", + "required": false, + "hidden": true + }, "active_frontend": { "type_widget": "bool_checkbox", "attribut_label": "Afficher dans le menu ?", diff --git a/backend/gn_module_monitoring/config/generic/observation.json b/backend/gn_module_monitoring/config/generic/observation.json index c15997d73..9263d5a63 100644 --- a/backend/gn_module_monitoring/config/generic/observation.json +++ b/backend/gn_module_monitoring/config/generic/observation.json @@ -4,7 +4,10 @@ "chained": true, "label": "Observation", "genre": "F", - "display_properties": ["cd_nom", "comments"], + "display_properties": [ + "cd_nom", + "comments" + ], "uuid_field_name": "uuid_observation", "generic": { "id_observation": { @@ -24,6 +27,11 @@ "hidden": true, "type_util": "user" }, + "id_individual": { + "type_widget": "text", + "attribut_label": "Id individu", + "hidden": true + }, "cd_nom": { "type_widget": "taxonomy", "attribut_label": "Espèce", @@ -44,4 +52,4 @@ "schema_dot_table": "gn_monitoring.t_observations" } } -} +} \ No newline at end of file diff --git a/backend/gn_module_monitoring/config/generic/site.json b/backend/gn_module_monitoring/config/generic/site.json index 4f0a52aed..d965891f6 100644 --- a/backend/gn_module_monitoring/config/generic/site.json +++ b/backend/gn_module_monitoring/config/generic/site.json @@ -6,7 +6,11 @@ "genre": "M", "geom_field_name": "geom", "uuid_field_name": "uuid_base_site", - "geometry_type": ["Point", "LineString", "Polygon"], + "geometry_type": [ + "Point", + "LineString", + "Polygon" + ], "display_properties": [ "base_site_name", "base_site_code", @@ -25,7 +29,8 @@ "last_visit", "id_inventor", "nb_visits", - "types_site" + "types_site", + "nb_individuals" ], "sorts": [ { @@ -80,6 +85,9 @@ "nb_visits": { "attribut_label": "Nb. visites" }, + "nb_individuals": { + "attribut_label": "Nb. individus" + }, "uuid_base_site": { "attribut_label": "uuid" }, @@ -115,4 +123,4 @@ "required": false } } -} +} \ No newline at end of file diff --git a/backend/gn_module_monitoring/config/repositories.py b/backend/gn_module_monitoring/config/repositories.py index 5251f7327..8b9d1736e 100644 --- a/backend/gn_module_monitoring/config/repositories.py +++ b/backend/gn_module_monitoring/config/repositories.py @@ -193,6 +193,7 @@ def get_config(module_code=None, force=False): "b_draw_sites_group", "taxonomy_display_field_name", "id_module", + "cd_nom", ]: var_name = "__MODULE.{}".format(field_name.upper()) config["custom"][var_name] = getattr(module, field_name) diff --git a/backend/gn_module_monitoring/migrations/0790c7d024fb_add_individuals.py b/backend/gn_module_monitoring/migrations/0790c7d024fb_add_individuals.py deleted file mode 100644 index ad8c573b3..000000000 --- a/backend/gn_module_monitoring/migrations/0790c7d024fb_add_individuals.py +++ /dev/null @@ -1,61 +0,0 @@ -"""add individuals - -Revision ID: 0790c7d024fb -Revises: fc90d31c677f -Create Date: 2023-09-27 14:01:26.035798 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects.postgresql import JSONB - - -# revision identifiers, used by Alembic. -revision = "0790c7d024fb" -down_revision = "fc90d31c677f" -branch_labels = None -depends_on = "84f40d008640" # individuals (geonature) - -SCHEMA = "gn_monitoring" - - -def upgrade(): - op.create_table( - "t_individual_complements", - sa.Column( - "id_base_individual", - sa.Integer, - sa.ForeignKey(f"{SCHEMA}.t_base_individuals.id_base_individual", ondelete="CASCADE"), - primary_key=True, - ), - sa.Column( - "id_module", - sa.Integer, - sa.ForeignKey("gn_commons.t_modules.id_module", ondelete="CASCADE"), - primary_key=True, - ), - sa.Column("data", JSONB), - schema=SCHEMA, - ) - - op.create_table( - "cor_individual_module", - sa.Column( - "id_base_individual", - sa.Integer, - sa.ForeignKey(f"{SCHEMA}.t_base_individuals.id_base_individual", ondelete="CASCADE"), - primary_key=True, - ), - sa.Column( - "id_module", - sa.Integer, - sa.ForeignKey("gn_commons.t_modules.id_module", ondelete="CASCADE"), - primary_key=True, - ), - schema=SCHEMA, - ) - - -def downgrade(): - op.drop_table("cor_individual_module", schema=SCHEMA) - op.drop_table("t_individual_complements", schema=SCHEMA) diff --git a/backend/gn_module_monitoring/migrations/2894b3c03c66_add_id_individual_col_t_observations.py b/backend/gn_module_monitoring/migrations/2894b3c03c66_add_id_individual_col_t_observations.py index 31d754327..96fa79b76 100644 --- a/backend/gn_module_monitoring/migrations/2894b3c03c66_add_id_individual_col_t_observations.py +++ b/backend/gn_module_monitoring/migrations/2894b3c03c66_add_id_individual_col_t_observations.py @@ -1,10 +1,11 @@ """add id_individual col t_observations Revision ID: 2894b3c03c66 -Revises: fc90d31c677f +Revises: 6a15625a0f4a Create Date: 2023-11-21 11:06:04.284038 """ + from alembic import op import sqlalchemy as sa from sqlalchemy.sql import column @@ -12,7 +13,7 @@ # revision identifiers, used by Alembic. revision = "2894b3c03c66" -down_revision = "fc90d31c677f" +down_revision = "6a15625a0f4a" branch_labels = None depends_on = "84f40d008640" # t_individuals (geonature) @@ -53,11 +54,13 @@ def upgrade(): def downgrade(): - op.execute(""" + op.execute( + """ UPDATE gn_monitoring.t_observations SET cd_nom = ind.cd_nom FROM gn_monitoring.t_individuals ind WHERE ind.id_individual = gn_monitoring.t_observations.id_individual; - """) + """ + ) op.drop_column(table_name=table, column_name=column_name, schema=monitorings_schema) op.alter_column( table_name=table, column_name=cd_nom_column_name, nullable=False, schema=monitorings_schema diff --git a/backend/gn_module_monitoring/migrations/398f94b364f7_add_cd_nom_t_module_complements.py b/backend/gn_module_monitoring/migrations/398f94b364f7_add_cd_nom_t_module_complements.py new file mode 100644 index 000000000..672db92bd --- /dev/null +++ b/backend/gn_module_monitoring/migrations/398f94b364f7_add_cd_nom_t_module_complements.py @@ -0,0 +1,36 @@ +"""add cd_nom t_module_complements + +Revision ID: 398f94b364f7 +Revises: 3ffeea74a9dd +Create Date: 2023-12-20 13:52:18.563621 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "398f94b364f7" +down_revision = "3ffeea74a9dd" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + schema="gn_monitoring", + table_name="t_module_complements", + column=sa.Column( + "cd_nom", + sa.Integer, + ), + ) + + +def downgrade(): + op.drop_column( + schema="gn_monitoring", + table_name="t_module_complements", + column_name="cd_nom", + ) diff --git a/backend/gn_module_monitoring/migrations/461b82ee737a_add_individual_permissions.py b/backend/gn_module_monitoring/migrations/461b82ee737a_add_individual_permissions.py index f1409a4ed..d686d5354 100644 --- a/backend/gn_module_monitoring/migrations/461b82ee737a_add_individual_permissions.py +++ b/backend/gn_module_monitoring/migrations/461b82ee737a_add_individual_permissions.py @@ -5,13 +5,14 @@ Create Date: 2023-11-21 14:14:48.084725 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '461b82ee737a' -down_revision = '2894b3c03c66' +revision = "461b82ee737a" +down_revision = "2894b3c03c66" branch_labels = None depends_on = None @@ -23,7 +24,7 @@ def upgrade(): VALUES ('MONITORINGS_INDIVIDUALS', 'Permissions sur les individus'), ('MONITORINGS_MARKINGS', 'Permissions sur les marquages'); """ - ) + ) def downgrade(): diff --git a/backend/gn_module_monitoring/migrations/c1528c94d350_upgrade_existing_permissions.py b/backend/gn_module_monitoring/migrations/c1528c94d350_upgrade_existing_permissions.py index 327e22844..11ad1d31e 100644 --- a/backend/gn_module_monitoring/migrations/c1528c94d350_upgrade_existing_permissions.py +++ b/backend/gn_module_monitoring/migrations/c1528c94d350_upgrade_existing_permissions.py @@ -1,7 +1,7 @@ """Upgrade existing permissions Revision ID: c1528c94d350 -Revises: 3ffeea74a9dd +Revises: 398f94b364f7 Create Date: 2023-10-02 12:09:53.695122 """ @@ -16,7 +16,7 @@ # revision identifiers, used by Alembic. revision = "c1528c94d350" -down_revision = "3ffeea74a9dd" +down_revision = "398f94b364f7" branch_labels = None depends_on = None diff --git a/backend/gn_module_monitoring/monitoring/definitions.py b/backend/gn_module_monitoring/monitoring/definitions.py index d4763329f..a722c994b 100644 --- a/backend/gn_module_monitoring/monitoring/definitions.py +++ b/backend/gn_module_monitoring/monitoring/definitions.py @@ -1,3 +1,5 @@ +from geonature.core.gn_monitoring.models import TIndividuals, TMarkingEvent + from gn_module_monitoring.monitoring.models import ( TMonitoringModules, TMonitoringSites, @@ -6,7 +8,11 @@ TMonitoringObservationDetails, TMonitoringSitesGroups, ) -from gn_module_monitoring.monitoring.objects import MonitoringModule, MonitoringSite +from gn_module_monitoring.monitoring.objects import ( + MonitoringModule, + MonitoringSite, + MonitoringIndividual, +) from gn_module_monitoring.monitoring.base import monitoring_definitions from gn_module_monitoring.monitoring.repositories import MonitoringObject from gn_module_monitoring.monitoring.geom import MonitoringObjectGeom @@ -25,6 +31,8 @@ "observation": TMonitoringObservations, "observation_detail": TMonitoringObservationDetails, "sites_group": TMonitoringSitesGroups, + "individual": TIndividuals, + "marking": TMarkingEvent, } MonitoringObjects_dict = { @@ -34,6 +42,8 @@ "observation": MonitoringObject, "observation_detail": MonitoringObject, "sites_group": MonitoringObjectGeom, + "individual": MonitoringIndividual, + "marking": MonitoringObject, } monitoring_definitions.set(MonitoringObjects_dict, MonitoringModels_dict) diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index 0594da176..a3655b36b 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -28,6 +28,9 @@ BibTypeSite, cor_visit_observer, TObservations, + TIndividuals, + TMarkingEvent, + corIndividualModule, ) from geonature.core.gn_meta.models import TDatasets from geonature.core.gn_commons.models import TModules, cor_module_dataset @@ -303,6 +306,21 @@ class TMonitoringSites(TBaseSites, PermissionModel, SitesQuery): geom_geojson = column_property(func.ST_AsGeoJSON(TBaseSites.geom), deferred=True) types_site = DB.relationship("BibTypeSite", secondary=cor_site_type, overlaps="sites") + nb_individuals = column_property( + select([func.count(func.distinct(TIndividuals.id_individual))]) + .join_from( + TBaseVisits, TObservations, TBaseVisits.id_base_visit == TObservations.id_base_visit + ) + .join_from( + TObservations, TIndividuals, TObservations.id_individual == TIndividuals.id_individual + ) + .where(TBaseVisits.id_base_site == id_base_site) + .correlate_except( + TBaseVisits + ) # Correlate permet d'éviter une répétition de la condition WHERE dans la sous requête + .scalar_subquery() + ) + @hybrid_property def last_visit(self): query = select(func.max(TBaseVisits.visit_date_min)).where( @@ -512,6 +530,7 @@ class TMonitoringModules(TModules, PermissionModel, MonitoringQuery): id_list_observer = DB.Column(DB.Integer) id_list_taxonomy = DB.Column(DB.Integer) + cd_nom = DB.Column(DB.Integer) taxonomy_display_field_name = DB.Column(DB.Unicode) b_synthese = DB.Column(DB.Boolean) @@ -547,14 +566,19 @@ class TMonitoringModules(TModules, PermissionModel, MonitoringQuery): "TDatasets", secondary=cor_module_dataset, join_depth=0, - overlaps="modules", + lazy="joined", ) - types_site = DB.relationship( - "BibTypeSite", - secondary=cor_module_type, + individuals = DB.relationship( + "TIndividuals", + lazy="select", + enable_typechecks=False, + secondary=corIndividualModule, + primaryjoin=(corIndividualModule.c.id_module == id_module), + secondaryjoin=(corIndividualModule.c.id_individual == TIndividuals.id_individual), + foreign_keys=[corIndividualModule.c.id_individual, corIndividualModule.c.id_module], + # viewonly=True, ) - data = DB.Column(JSONB) # visits = DB.relationship( @@ -572,3 +596,32 @@ class TMonitoringModules(TModules, PermissionModel, MonitoringQuery): cascade="all", overlaps="sites,sites_group,module", ) + + +TIndividuals.nb_sites = column_property( + select([func.count(func.distinct(TMonitoringSites.id_base_site))]) + .where( + and_( + TObservations.id_individual == TIndividuals.id_individual, + TObservations.id_base_visit == TMonitoringVisits.id_base_visit, + TBaseVisits.id_base_site == TMonitoringSites.id_base_site, + ) + ) + .correlate_except(TMonitoringSites) + .scalar_subquery() +) +# NOTES: [SUIVI_INDIVIDU] pourquoi c'est nécessaire de le garder ici ? +TMonitoringSites.nb_individuals = column_property( + select([func.count(func.distinct(TIndividuals.id_individual))]) + .join_from( + TBaseVisits, TObservations, TBaseVisits.id_base_visit == TObservations.id_base_visit + ) + .join_from( + TObservations, TIndividuals, TObservations.id_individual == TIndividuals.id_individual + ) + .where(TBaseVisits.id_base_site == TMonitoringSites.id_base_site) + .correlate_except( + TBaseVisits + ) # Correlate permet d'éviter une répétition de la condition WHERE dans la sous requête + .scalar_subquery() +) diff --git a/backend/gn_module_monitoring/monitoring/objects.py b/backend/gn_module_monitoring/monitoring/objects.py index 78b9eaae0..59e42e537 100644 --- a/backend/gn_module_monitoring/monitoring/objects.py +++ b/backend/gn_module_monitoring/monitoring/objects.py @@ -1,3 +1,4 @@ +from geonature.utils.errors import GeoNatureError from geonature.utils.env import DB from gn_module_monitoring.monitoring.repositories import MonitoringObject @@ -53,3 +54,23 @@ def preprocess_data(self, properties, data=[]): # ] # 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 + + +class MonitoringIndividual(MonitoringObject): + """ + PATCH + pour pouvoir renseigner la table cor_individual_module + avec la méthode from_dict + """ + + def get_value_specific(self, param_name): + # DO NOT LOAD data here + pass + + 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) + + data["modules"] = module_ids diff --git a/backend/gn_module_monitoring/monitoring/repositories.py b/backend/gn_module_monitoring/monitoring/repositories.py index a7f79eff1..8423099cb 100644 --- a/backend/gn_module_monitoring/monitoring/repositories.py +++ b/backend/gn_module_monitoring/monitoring/repositories.py @@ -12,6 +12,9 @@ from gn_module_monitoring.monitoring.models import PermissionModel, TMonitoringModules import logging +from ..utils.utils import to_int +from .base import monitoring_definitions +from sqlalchemy.orm import joinedload log = logging.getLogger(__name__) diff --git a/backend/gn_module_monitoring/monitoring/serializer.py b/backend/gn_module_monitoring/monitoring/serializer.py index bcc2b3b94..8f686d592 100644 --- a/backend/gn_module_monitoring/monitoring/serializer.py +++ b/backend/gn_module_monitoring/monitoring/serializer.py @@ -9,6 +9,9 @@ from geonature.utils.env import DB from geonature.core.gn_permissions.tools import get_scopes_by_action +from geonature.core.gn_monitoring.models import ( + TIndividuals, +) from gn_module_monitoring.utils.utils import to_int from gn_module_monitoring.routes.data_utils import id_field_name_dict from gn_module_monitoring.utils.routes import get_objet_with_permission_boolean @@ -284,8 +287,24 @@ def serialize(self, depth=1, is_child=False): return monitoring_object_dict def preprocess_data(self, data): - # a redefinir dans la classe - pass + + # Query TIndividuals to get the cd_nom + if ( + self._object_type == "observation" + and data["cd_nom"] is None + and data["id_individual"] is not None + or ( + self._object_type == "observation" + and self._id is not None + and data["id_individual"] is not None + ) + ): + individual = TIndividuals.query.get(data["id_individual"]) + if individual is None: + raise ValueError("TIndividuals with provided id_individual not found") + else: + data["cd_nom"] = individual.cd_nom + return data def populate(self, post_data): # pour la partie sur les relationships mettre le from_dict dans utils_flask_sqla ??? From f38789dcd65ca8095238a4e544b069e86e53bcd2 Mon Sep 17 00:00:00 2001 From: Maxime Vergez Date: Wed, 22 Nov 2023 17:08:13 +0100 Subject: [PATCH 03/12] docs: add individual docs docs: fix details and add more docs: add permissions section docs: update docs following required id_module And new cd_nom setting docs: more doc for individiuals --- docs/images/2023-11-MCD-individuals.png | Bin 0 -> 24936 bytes docs/images/individual_widget.png | Bin 0 -> 6060 bytes docs/images/individual_widget_create.png | Bin 0 -> 8658 bytes docs/individuals.md | 188 +++++++++++++++++++++++ docs/sous_module.md | 9 +- 5 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 docs/images/2023-11-MCD-individuals.png create mode 100644 docs/images/individual_widget.png create mode 100644 docs/images/individual_widget_create.png create mode 100644 docs/individuals.md diff --git a/docs/images/2023-11-MCD-individuals.png b/docs/images/2023-11-MCD-individuals.png new file mode 100644 index 0000000000000000000000000000000000000000..55f0a35728210427c797358b872d91541befc757 GIT binary patch literal 24936 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 literal 0 HcmV?d00001 diff --git a/docs/images/individual_widget.png b/docs/images/individual_widget.png new file mode 100644 index 0000000000000000000000000000000000000000..fb04cbb40438a581925590ae39024d0b805c0a50 GIT binary patch literal 6060 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 K~m-i9*06qc;Xl23G?OOj2Cx4$C#c{+L#^(hY% zogc{2$O=1KD!6IhVzu0F9HeO*Ej8Hw!sy-7z%ViaxzB>#-#>0i7G5B!w;WhlTKd2w zc)H2G4unr82&Z^nLqi!%ipvlb9|D%*efD#{L*bMc zECcNfx5?8>|L1Wo_R-&mo!)?!G=kUF)$LcV@BBwKsAraJtk(D}{yVWX96{~wzVpv) zHR#Cz(kK7@It<|IM)Y<~wx@bzmz1dNZ!BRq1+&^ldyU`-;x}QSzt;lh2=+T)B7#_3 z9yeh&EMfT9hoAr zBR(t(nAjLT+>3w#q5fkCc=3u@HlQy5+%P2#4i3g+BSrFE%X+BPeGLpgszHp7sI0B! zq5-w??~F}&A8&71;+J9X z^v)&{i6}Hp)}^R9JJ)6TxDm1*ICP>brDG#y;+{Qv@yWll!nf=>7DYjvQvpJA=Tr^4 zrH6)q5!W9-Tfrm*2=B(n$B!lVbJL&M<4$(gma$io!MIlf`2s4{?moyHK$81N-QdMF z%Vo9$&9-R+-~{~|Cff&x!}<7-xM}%bbr0K9rLJB!^b!CvpsudXNzOy|l4>k1!tzrt zHq$ob6NmJzClRF3(dY-OLw-+4wa@ck~>(VvvE#zIXkx z{97#Qpy7MOK-m$#6Lel_n%vW7o9iq`z7EEFPnN#rN8OwN5@8tp8t8|Ul9G5Z30HO! zmj7Rx$3|^&bFe(F@di%a%I|$Z(1kSVXQ7pvqS=rt`jgaPYd%UNq&nqMgEhU2Knerb z1SD%>?CSs|HURdzx)1Drdhz%m3?2}&2gV;9eUDg3HrUX;TVrwz?W=X8csrv?{DKol zP-r5*nVl}p-5E?=7 z4ERqfX=G2&(Vq2oEiRV+%{RU1joaCw7GOX(=NVoP-b?}xI9P;7R#rlzS8CUq5pg7z zG-_8Dy0DM@8#qSUF?#D-cJ|oD#O3x4#PH{O?Stg1FM-JNl5?)EZ7TCe|D6A3eU1yQ{n^Z)<= literal 0 HcmV?d00001 diff --git a/docs/images/individual_widget_create.png b/docs/images/individual_widget_create.png new file mode 100644 index 0000000000000000000000000000000000000000..a3f3ae1ec143ecde49726f34bfb3c30197b2f141 GIT binary patch literal 8658 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 literal 0 HcmV?d00001 diff --git a/docs/individuals.md b/docs/individuals.md new file mode 100644 index 000000000..6af14b70d --- /dev/null +++ b/docs/individuals.md @@ -0,0 +1,188 @@ +# Gestion des individus + +## Introduction +Le suivi d'individu, tel qu'il est implémenté dans le module monitoring, +permet de : + +- Créer des individus +- Créer des marquages associés aux individus (un individu peut + avoir plusieurs marquages) +- Partager des individus entre les modules + +## Base de données + +Le schéma de base de données est le suivant : + +![MCD](images/2023-11-MCD-individuals.png) + +Les tables en grises sont présentes à des fins de compréhension. + +## Nomenclature + +Le type de marquage est stocké comme une nomenclature dont le type +est `TYP_MARQUAGE`. Ce type a été spécialement créé pour le marquage des +individus. + +## Implémentation dans le module + + +## Objets à déclarer +Il y a donc 2 objets déclarés dans le module : +- `individual` +- `marking` + +L'objet marking doit être un enfant de l'objet individual. Dans +le fichier `config.json` d'un sous module, il suffit de déclarer +le `tree` comme suit : + +```json +"tree": { + "module": { + "site": { + "visit": { + "observation": null + } + }, + "individual": { + "marking": null + } + } + } +``` + +L'objet individual peut-être inséré comme tel pour créer un onglet +au même niveau que le site afin d'avoir la liste d'individus +facilement accessible. + +Il n'est actuellement pas possible de renseigner de champs personnalisés +sur les individus via la création d'un fichier `individual.json` +comme c'est le cas pour les autres objets. +Cette impossibilité émane du fait que le [widget individu](#le-widget-individu), +créé côté GeoNature, propose un formulaire de création disposant de champs +fixes qui ne doit donc différer du formulaire côté monitoring. + +## Le widget individu + +Ce widget, disponible dans les composants de GeoNature, permet de : + +- Sélectionner un individu déjà présent +- Créer un nouvel individu + +Il se présente comme tel : + +![Widget](images/individual_widget.png) + +Et en cliquant sur le "+", un formulaire de création d'individu apparaîtra : + +![WidgetCreate](images/individual_widget_create.png) + + +Il est paramétrable en json comme ceci : + +```json +"id_individual": { + "type_widget": "individuals", + "attribut_label": "Choix de l'individu", + "id_module": "__MODULE.ID_MODULE", + "id_list": "__MODULE.ID_LIST_TAXONOMY", + "cd_nom": "__MODULE.CD_NOM" +} +``` + +Les attributs sont optionnels si le contraire n'est pas spécifié et sont les suivants : + +- `id_module` (**obligatoire**) : permet de spécifier le module auxquel + doivent être rattachés les individus proposés dans le menu déroulant. + Il est obligatoire pour assurer le calcul de permissions de + l'utilisateur en "Read" et en "Create". +- `id_list` : dans le formulaire de saisie, restreint la saisie d'espèce à + une liste taxonomique +- `cd_nom` : fixe le champ Taxon au cd_nom donné et donc ne le fait pas + apparaître dans le formulaire. + +## Cas du protocole mono-spécifique + +Il est possible de renseigner une seule espèce pour un protocole. +Comme spécifié dans la documentation du sous module, une variable +`__MODULE.CD_NOM` est disponible pour renseigner un même `cd_nom` +pour chaque widget. + +Dans le cas des individus, le fichier `config.json` doit +paramétrer un champ `cd_nom` et masquer le champ `id_list_taxonomy` +(qui devient inutile si une seule espèce est définie) : + +```json +{ + "module_label": "Test", + "module_desc": "Module de test individus", + "specific": { + "cd_nom": { + "type_widget": "taxonomy", + "attribut_label": "Espèce", + "type_util": "taxonomy", + "required": true + }, + "id_list_taxonomy": { + "hidden": true + } + } +} +``` +Le fichier observation.json peut donc être écrit de cette manière : + +```json +{ + "specific": { + "cd_nom": { + "type_widget": "text", + "required": false, + "hidden": true + }, + "id_individual": { + "type_widget": "individuals", + "attribut_label": "Choix de l'individu", + "id_module": "__MODULE.ID_MODULE", + "id_list": "__MODULE.ID_LIST_TAXONOMY", + "cd_nom": "__MODULE.CD_NOM", + "hidden": false + } + } +} +``` + + +## Cas de l'observation + +Pour que les individus soient implémentés dans le module, la contrainte +`NOT NULL` sur la colonne `cd_nom` de `gn_monitoring.t_observations` a dû +être supprimée au profit d'une contrainte `NOT NULL` sur la colonne `cd_nom` +**OU** la nouvelle colonne `id_individual`. + +Pour pouvoir donc saisir des individus au lieu d'espèces dans une observation, +la configuration minimale du fichier `observation.json` doit être la suivante : + +```json +{ + "specific": { + "cd_nom": { + "type_widget": "text", + "required": false, + "hidden": true + }, + "id_individual": { + "type_widget": "individuals", + "attribut_label": "Choix de l'individu", + "id_module": "__MODULE.ID_MODULE", + "id_list": "__MODULE.ID_LIST_TAXONOMY", + "hidden": false + } + } +} +``` + +Elle permet de désactiver la saisie du `cd_nom` au profit de l'individu. + +## Permissions + +Comme tout objet monitoring, des permissions seront ajoutées à l'installation +pour CRUD sur les objets `MONITORINGS_INDIVIDUALS` et `MONITORINGS_MARKINGS`. diff --git a/docs/sous_module.md b/docs/sous_module.md index 1b8e8cc36..dd0018dec 100644 --- a/docs/sous_module.md +++ b/docs/sous_module.md @@ -209,6 +209,7 @@ Pour cela il faut utiliser les variables suivantes : * `__MODULE.TAXONOMY_DISPLAY_FIELD_NAME` * `__MODULE.TYPES_SITE` * `__MODULE.IDS_TYPES_SITE` +* `__MODULE.CD_NOM` qui peuvent servir dans la définition des formulaires (en particulier pour les datalist). Voir ci dessous @@ -216,7 +217,7 @@ pour les datalist). Voir ci dessous #### Liste des widgets disponibles | Widgets | Commentaire | -|--------------|--------------------------------------------------------------------------| +| ------------ | ------------------------------------------------------------------------ | | text | Texte sur une seule ligne | | textarea | Texte sur une plusieurs lignes | | radio | Choix multiples uniques | @@ -236,9 +237,9 @@ pour les datalist). Voir ci dessous #### Listes des paramètres disponibles par type de widgets : | Widgets | Paramètres | Commentaire | -|-----------------------------------------|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| +| --------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | Tous | attribut_label | Label du formulaire | -| Tous | definition | Ajoute une tooltip avec le contenu de ce paramètre (le paramètre `link_definition` ne doit pas être défini) | +| Tous | definition | Ajoute une tooltip avec le contenu de ce paramètre (le paramètre `link_definition` ne doit pas être défini) | | Tous | required | Booléen : permet de rendre obligatoire cet input | | Tous | hidden | Booléen : permet de cacher un formulaire | | Tous | link_definition | Ajoute un lien vers l'addresse pointé par ce paramètre. Le paramètre `definition` doit également être définit | @@ -252,7 +253,7 @@ pour les datalist). Voir ci dessous | nomenclature | cd_nomenclatures | Liste des codes nomenclatures à afficher (afin d'éliminer certains items de nomenclatures que l'on ne veut pas pour ce sous-module) | | nomenclature / dataset | multi_select | Booléan : permet de seléctionner plusieurs items de nomenclatures | | dataset | module_code | Limite aux jeu de données associés à ce module | -| html | html | Contenu du bloc html | +| html | html | Contenu du bloc html | ## Définir une nouvelle variable From bbf9266d268b86c13569a8846d33ca2f4df993c9 Mon Sep 17 00:00:00 2001 From: amandine-sahl Date: Tue, 31 Dec 2024 16:52:56 +0100 Subject: [PATCH 04/12] [DB] Reorder permission --- .../2894b3c03c66_add_id_individual_col_t_observations.py | 6 +++--- .../398f94b364f7_add_cd_nom_t_module_complements.py | 6 +++--- .../migrations/461b82ee737a_add_individual_permissions.py | 2 +- .../migrations/c1528c94d350_upgrade_existing_permissions.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/gn_module_monitoring/migrations/2894b3c03c66_add_id_individual_col_t_observations.py b/backend/gn_module_monitoring/migrations/2894b3c03c66_add_id_individual_col_t_observations.py index 96fa79b76..300c899cf 100644 --- a/backend/gn_module_monitoring/migrations/2894b3c03c66_add_id_individual_col_t_observations.py +++ b/backend/gn_module_monitoring/migrations/2894b3c03c66_add_id_individual_col_t_observations.py @@ -1,7 +1,7 @@ -"""add id_individual col t_observations +"""[individuals] add id_individual col t_observations Revision ID: 2894b3c03c66 -Revises: 6a15625a0f4a +Revises: 398f94b364f7 Create Date: 2023-11-21 11:06:04.284038 """ @@ -13,7 +13,7 @@ # revision identifiers, used by Alembic. revision = "2894b3c03c66" -down_revision = "6a15625a0f4a" +down_revision = "398f94b364f7" branch_labels = None depends_on = "84f40d008640" # t_individuals (geonature) diff --git a/backend/gn_module_monitoring/migrations/398f94b364f7_add_cd_nom_t_module_complements.py b/backend/gn_module_monitoring/migrations/398f94b364f7_add_cd_nom_t_module_complements.py index 672db92bd..8906bf1bd 100644 --- a/backend/gn_module_monitoring/migrations/398f94b364f7_add_cd_nom_t_module_complements.py +++ b/backend/gn_module_monitoring/migrations/398f94b364f7_add_cd_nom_t_module_complements.py @@ -1,7 +1,7 @@ -"""add cd_nom t_module_complements +"""[individuals] add cd_nom t_module_complements Revision ID: 398f94b364f7 -Revises: 3ffeea74a9dd +Revises: 6f90dd1aaf69 Create Date: 2023-12-20 13:52:18.563621 """ @@ -12,7 +12,7 @@ # revision identifiers, used by Alembic. revision = "398f94b364f7" -down_revision = "3ffeea74a9dd" +down_revision = "6f90dd1aaf69" branch_labels = None depends_on = None diff --git a/backend/gn_module_monitoring/migrations/461b82ee737a_add_individual_permissions.py b/backend/gn_module_monitoring/migrations/461b82ee737a_add_individual_permissions.py index d686d5354..8a5df06ea 100644 --- a/backend/gn_module_monitoring/migrations/461b82ee737a_add_individual_permissions.py +++ b/backend/gn_module_monitoring/migrations/461b82ee737a_add_individual_permissions.py @@ -1,4 +1,4 @@ -"""add individual permissions +"""[individuals] add individual permissions Revision ID: 461b82ee737a Revises: 2894b3c03c66 diff --git a/backend/gn_module_monitoring/migrations/c1528c94d350_upgrade_existing_permissions.py b/backend/gn_module_monitoring/migrations/c1528c94d350_upgrade_existing_permissions.py index 11ad1d31e..327e22844 100644 --- a/backend/gn_module_monitoring/migrations/c1528c94d350_upgrade_existing_permissions.py +++ b/backend/gn_module_monitoring/migrations/c1528c94d350_upgrade_existing_permissions.py @@ -1,7 +1,7 @@ """Upgrade existing permissions Revision ID: c1528c94d350 -Revises: 398f94b364f7 +Revises: 3ffeea74a9dd Create Date: 2023-10-02 12:09:53.695122 """ @@ -16,7 +16,7 @@ # revision identifiers, used by Alembic. revision = "c1528c94d350" -down_revision = "398f94b364f7" +down_revision = "3ffeea74a9dd" branch_labels = None depends_on = None From c0014c9a754848bf8e676c5cdd2b12a93645ecf9 Mon Sep 17 00:00:00 2001 From: amandine-sahl Date: Tue, 31 Dec 2024 16:53:39 +0100 Subject: [PATCH 05/12] [models] Correction et integration CRUVED + Serialization marshmallow --- .../monitoring/definitions.py | 8 +- .../gn_module_monitoring/monitoring/models.py | 74 +++++++++++-------- .../monitoring/objects.py | 3 - .../monitoring/schemas.py | 10 +++ .../monitoring/serializer.py | 9 ++- .../gn_module_monitoring/routes/data_utils.py | 1 + 6 files changed, 64 insertions(+), 41 deletions(-) diff --git a/backend/gn_module_monitoring/monitoring/definitions.py b/backend/gn_module_monitoring/monitoring/definitions.py index a722c994b..91721ef84 100644 --- a/backend/gn_module_monitoring/monitoring/definitions.py +++ b/backend/gn_module_monitoring/monitoring/definitions.py @@ -1,5 +1,3 @@ -from geonature.core.gn_monitoring.models import TIndividuals, TMarkingEvent - from gn_module_monitoring.monitoring.models import ( TMonitoringModules, TMonitoringSites, @@ -7,6 +5,8 @@ TMonitoringObservations, TMonitoringObservationDetails, TMonitoringSitesGroups, + TMonitoringIndividuals, + TMonitoringMarkingEvent, ) from gn_module_monitoring.monitoring.objects import ( MonitoringModule, @@ -31,8 +31,8 @@ "observation": TMonitoringObservations, "observation_detail": TMonitoringObservationDetails, "sites_group": TMonitoringSitesGroups, - "individual": TIndividuals, - "marking": TMarkingEvent, + "individual": TMonitoringIndividuals, + "marking": TMonitoringMarkingEvent, } MonitoringObjects_dict = { diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index a3655b36b..badf62069 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -304,6 +304,7 @@ class TMonitoringSites(TBaseSites, PermissionModel, SitesQuery): ) geom_geojson = column_property(func.ST_AsGeoJSON(TBaseSites.geom), deferred=True) + types_site = DB.relationship("BibTypeSite", secondary=cor_site_type, overlaps="sites") nb_individuals = column_property( @@ -562,23 +563,28 @@ class TMonitoringModules(TModules, PermissionModel, MonitoringQuery): sites_groups = DB.relationship(TMonitoringSitesGroups, secondary=cor_sites_group_module) - datasets = DB.relationship( - "TDatasets", - secondary=cor_module_dataset, - join_depth=0, - lazy="joined", - ) - individuals = DB.relationship( - "TIndividuals", + "TMonitoringIndividuals", lazy="select", enable_typechecks=False, secondary=corIndividualModule, primaryjoin=(corIndividualModule.c.id_module == id_module), secondaryjoin=(corIndividualModule.c.id_individual == TIndividuals.id_individual), foreign_keys=[corIndividualModule.c.id_individual, corIndividualModule.c.id_module], + overlaps="modules", # viewonly=True, ) + + datasets = DB.relationship( + "TDatasets", + secondary=cor_module_dataset, + join_depth=0, + overlaps="modules", + ) + types_site = DB.relationship( + "BibTypeSite", + secondary=cor_module_type, + ) data = DB.Column(JSONB) # visits = DB.relationship( @@ -598,30 +604,34 @@ class TMonitoringModules(TModules, PermissionModel, MonitoringQuery): ) -TIndividuals.nb_sites = column_property( - select([func.count(func.distinct(TMonitoringSites.id_base_site))]) - .where( - and_( - TObservations.id_individual == TIndividuals.id_individual, - TObservations.id_base_visit == TMonitoringVisits.id_base_visit, - TBaseVisits.id_base_site == TMonitoringSites.id_base_site, +@serializable +class TMonitoringMarkingEvent(TMarkingEvent, PermissionModel, MonitoringQuery): + pass + + +@serializable +class TMonitoringIndividuals(TIndividuals, PermissionModel, MonitoringQuery): + + nb_sites = column_property( + select([func.count(func.distinct(TMonitoringSites.id_base_site))]) + .join_from( + TObservations, TBaseVisits, TBaseVisits.id_base_visit == TObservations.id_base_visit ) + .join_from( + TBaseVisits, + TMonitoringSites, + TMonitoringSites.id_base_site == TBaseVisits.id_base_site, + ) + .where(TObservations.id_individual == TIndividuals.id_individual) + .correlate_except( + TBaseVisits + ) # Correlate permet d'éviter une répétition de la condition WHERE dans la sous requête + .scalar_subquery() ) - .correlate_except(TMonitoringSites) - .scalar_subquery() -) -# NOTES: [SUIVI_INDIVIDU] pourquoi c'est nécessaire de le garder ici ? -TMonitoringSites.nb_individuals = column_property( - select([func.count(func.distinct(TIndividuals.id_individual))]) - .join_from( - TBaseVisits, TObservations, TBaseVisits.id_base_visit == TObservations.id_base_visit - ) - .join_from( - TObservations, TIndividuals, TObservations.id_individual == TIndividuals.id_individual + + # Redéfinition de la relation marking pour utiliser la classe TMonitoringMarkingEvent + # qui hérite de PermissionModel pour le CRUVED + markings = DB.relationship( + TMonitoringMarkingEvent, + primaryjoin=(TIndividuals.id_individual == TMonitoringMarkingEvent.id_individual), ) - .where(TBaseVisits.id_base_site == TMonitoringSites.id_base_site) - .correlate_except( - TBaseVisits - ) # Correlate permet d'éviter une répétition de la condition WHERE dans la sous requête - .scalar_subquery() -) diff --git a/backend/gn_module_monitoring/monitoring/objects.py b/backend/gn_module_monitoring/monitoring/objects.py index 59e42e537..b6e6292a2 100644 --- a/backend/gn_module_monitoring/monitoring/objects.py +++ b/backend/gn_module_monitoring/monitoring/objects.py @@ -1,6 +1,3 @@ -from geonature.utils.errors import GeoNatureError -from geonature.utils.env import DB - from gn_module_monitoring.monitoring.repositories import MonitoringObject from gn_module_monitoring.monitoring.geom import MonitoringObjectGeom diff --git a/backend/gn_module_monitoring/monitoring/schemas.py b/backend/gn_module_monitoring/monitoring/schemas.py index b33e37c89..5bed12fa6 100644 --- a/backend/gn_module_monitoring/monitoring/schemas.py +++ b/backend/gn_module_monitoring/monitoring/schemas.py @@ -18,6 +18,7 @@ TMonitoringModules, TMonitoringObservations, TMonitoringObservationDetails, + TMonitoringIndividuals, ) @@ -196,3 +197,12 @@ class Meta: load_relationships = True medias = MA.Nested(MediaSchema, many=True) + + +class MonitoringIndividualsSchema(MA.SQLAlchemyAutoSchema): + class Meta: + model = TMonitoringIndividuals + include_fk = True + load_relationships = True + + medias = MA.Nested(MediaSchema, many=True) diff --git a/backend/gn_module_monitoring/monitoring/serializer.py b/backend/gn_module_monitoring/monitoring/serializer.py index 8f686d592..ccd75d4ee 100644 --- a/backend/gn_module_monitoring/monitoring/serializer.py +++ b/backend/gn_module_monitoring/monitoring/serializer.py @@ -12,6 +12,7 @@ from geonature.core.gn_monitoring.models import ( TIndividuals, ) +from geonature.core.gn_monitoring.schema import TMarkingEventSchema from gn_module_monitoring.utils.utils import to_int from gn_module_monitoring.routes.data_utils import id_field_name_dict from gn_module_monitoring.utils.routes import get_objet_with_permission_boolean @@ -24,6 +25,7 @@ MonitoringVisitsSchema, MonitoringObservationsSchema, MonitoringObservationsDetailsSchema, + MonitoringIndividualsSchema, ) MonitoringSerializer_dict = { @@ -33,6 +35,8 @@ "sites_group": MonitoringSitesGroupsSchema, "observation": MonitoringObservationsSchema, "observation_detail": MonitoringObservationsDetailsSchema, + "individual": MonitoringIndividualsSchema, + "marking": TMarkingEventSchema, } @@ -111,6 +115,7 @@ def unflatten_specific_properties(self, properties): not is_in_model and prop not in self.config_schema("generic").keys() and prop != "id_module" + and prop != "data" ): properties["data"][prop] = properties.pop(prop) @@ -222,8 +227,8 @@ def serialize(self, depth=1, is_child=False): for k in display_properties if k in module_config[self._object_type]["specific"].keys() ] - - display_generic.append("data") + if hasattr(self._model, "data"): + display_generic.append("data") display_generic.append(self.config_param("id_field_name")) # Sérialisation de l'objet diff --git a/backend/gn_module_monitoring/routes/data_utils.py b/backend/gn_module_monitoring/routes/data_utils.py index a398d2260..c1c16dde4 100644 --- a/backend/gn_module_monitoring/routes/data_utils.py +++ b/backend/gn_module_monitoring/routes/data_utils.py @@ -88,6 +88,7 @@ def get_init_data(module_code): out["nomenclature"] = [] for code_type in data.get("nomenclature"): nomenclature_list = get_nomenclature_list(code_type=code_type) + # TODO : exception quand pas de valeur for nomenclature in nomenclature_list["values"]: nomenclature["code_type"] = code_type out["nomenclature"].append(nomenclature) From ab8106a2ab8461e3ba9939cccaea5dc63263e181 Mon Sep 17 00:00:00 2001 From: amandine-sahl Date: Tue, 31 Dec 2024 17:06:07 +0100 Subject: [PATCH 06/12] Bump GeoNature --- dependencies/GeoNature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/GeoNature b/dependencies/GeoNature index 30c272664..9576fc376 160000 --- a/dependencies/GeoNature +++ b/dependencies/GeoNature @@ -1 +1 @@ -Subproject commit 30c27266495b4affc635f79748c9984feb81a6d7 +Subproject commit 9576fc3765dc6f01892bba88d6b96817e55f7739 From f4d98cde0f8e56b7e69f7f2a02139cc4f5bf9c07 Mon Sep 17 00:00:00 2001 From: amandine-sahl Date: Thu, 2 Jan 2025 09:48:48 +0100 Subject: [PATCH 07/12] Move migration to gn2 --- ...66_add_id_individual_col_t_observations.py | 68 ------------------- ...461b82ee737a_add_individual_permissions.py | 4 +- 2 files changed, 2 insertions(+), 70 deletions(-) delete mode 100644 backend/gn_module_monitoring/migrations/2894b3c03c66_add_id_individual_col_t_observations.py diff --git a/backend/gn_module_monitoring/migrations/2894b3c03c66_add_id_individual_col_t_observations.py b/backend/gn_module_monitoring/migrations/2894b3c03c66_add_id_individual_col_t_observations.py deleted file mode 100644 index 300c899cf..000000000 --- a/backend/gn_module_monitoring/migrations/2894b3c03c66_add_id_individual_col_t_observations.py +++ /dev/null @@ -1,68 +0,0 @@ -"""[individuals] add id_individual col t_observations - -Revision ID: 2894b3c03c66 -Revises: 398f94b364f7 -Create Date: 2023-11-21 11:06:04.284038 - -""" - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.sql import column - - -# revision identifiers, used by Alembic. -revision = "2894b3c03c66" -down_revision = "398f94b364f7" -branch_labels = None -depends_on = "84f40d008640" # t_individuals (geonature) - -monitorings_schema = "gn_monitoring" -table = "t_observations" -column_name = "id_individual" -foreign_schema = monitorings_schema -foreign_table = "t_individuals" -foreign_key = column_name - -constraint_name = f"check_{table}_cd_nom_or_id_individual_not_null" -cd_nom_column_name = "cd_nom" - - -def upgrade(): - op.add_column( - table, - sa.Column( - column_name, - sa.Integer(), - sa.ForeignKey( - f"{foreign_schema}.{foreign_table}.{foreign_key}", - name=f"fk_{table}_{column_name}", - onupdate="CASCADE", - ), - ), - schema=monitorings_schema, - ) - op.alter_column( - table_name=table, column_name=cd_nom_column_name, nullable=True, schema=monitorings_schema - ) - op.create_check_constraint( - table_name=table, - constraint_name=constraint_name, - condition=sa.or_(column(cd_nom_column_name).isnot(None), column(column_name).isnot(None)), - schema=monitorings_schema, - ) - - -def downgrade(): - op.execute( - """ - UPDATE gn_monitoring.t_observations SET cd_nom = ind.cd_nom - FROM gn_monitoring.t_individuals ind - WHERE ind.id_individual = gn_monitoring.t_observations.id_individual; - """ - ) - op.drop_column(table_name=table, column_name=column_name, schema=monitorings_schema) - op.alter_column( - table_name=table, column_name=cd_nom_column_name, nullable=False, schema=monitorings_schema - ) - # constraint automatically dropped with drop_column above diff --git a/backend/gn_module_monitoring/migrations/461b82ee737a_add_individual_permissions.py b/backend/gn_module_monitoring/migrations/461b82ee737a_add_individual_permissions.py index 8a5df06ea..6115ad5a0 100644 --- a/backend/gn_module_monitoring/migrations/461b82ee737a_add_individual_permissions.py +++ b/backend/gn_module_monitoring/migrations/461b82ee737a_add_individual_permissions.py @@ -1,7 +1,7 @@ """[individuals] add individual permissions Revision ID: 461b82ee737a -Revises: 2894b3c03c66 +Revises: 398f94b364f7 Create Date: 2023-11-21 14:14:48.084725 """ @@ -12,7 +12,7 @@ # revision identifiers, used by Alembic. revision = "461b82ee737a" -down_revision = "2894b3c03c66" +down_revision = "398f94b364f7" branch_labels = None depends_on = None From 4a8b17c26c88573c757b30e9cefc3c9da413a705 Mon Sep 17 00:00:00 2001 From: amandine-sahl Date: Thu, 2 Jan 2025 15:57:31 +0100 Subject: [PATCH 08/12] =?UTF-8?q?[config]=20Suppression=20param=C3=A8tres?= =?UTF-8?q?=20obsol=C3=A8tes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/gn_module_monitoring/config/generic/individual.json | 1 - backend/gn_module_monitoring/config/generic/marking.json | 5 ----- 2 files changed, 6 deletions(-) diff --git a/backend/gn_module_monitoring/config/generic/individual.json b/backend/gn_module_monitoring/config/generic/individual.json index 8c07192fa..c63daf6ed 100644 --- a/backend/gn_module_monitoring/config/generic/individual.json +++ b/backend/gn_module_monitoring/config/generic/individual.json @@ -1,5 +1,4 @@ { - "cruved": {"C":1, "U":1, "D": 1}, "id_field_name": "id_individual", "description_field_name": "individual_name", "chained": true, diff --git a/backend/gn_module_monitoring/config/generic/marking.json b/backend/gn_module_monitoring/config/generic/marking.json index 8dfa63f15..a958b68d2 100644 --- a/backend/gn_module_monitoring/config/generic/marking.json +++ b/backend/gn_module_monitoring/config/generic/marking.json @@ -1,9 +1,4 @@ { - "cruved": { - "C": 1, - "U": 1, - "D": 1 - }, "id_field_name": "id_marking", "description_field_name": "id_marking", "chained": true, From 8c894c5c5e25a57c1434d9ffa5eda04d6cf19783 Mon Sep 17 00:00:00 2001 From: Camille Monchicourt Date: Fri, 3 Jan 2025 11:41:20 +0100 Subject: [PATCH 09/12] Review doc individuals.md --- docs/individuals.md | 50 ++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/individuals.md b/docs/individuals.md index 6af14b70d..dbb596b39 100644 --- a/docs/individuals.md +++ b/docs/individuals.md @@ -1,13 +1,13 @@ # Gestion des individus ## Introduction -Le suivi d'individu, tel qu'il est implémenté dans le module monitoring, -permet de : + +Le suivi d'individus dans Monitoring permet de : - Créer des individus - Créer des marquages associés aux individus (un individu peut avoir plusieurs marquages) -- Partager des individus entre les modules +- Partager des individus entre les modules (pas encore possible ?) ## Base de données @@ -15,24 +15,25 @@ Le schéma de base de données est le suivant : ![MCD](images/2023-11-MCD-individuals.png) -Les tables en grises sont présentes à des fins de compréhension. +> **_NOTE:_** Les tables en gris sont affichées à des fins de compréhension. ## Nomenclature -Le type de marquage est stocké comme une nomenclature dont le type +Le type de marquage est stocké dans une nomenclature dont le type est `TYP_MARQUAGE`. Ce type a été spécialement créé pour le marquage des individus. ## Implémentation dans le module +### Objets à déclarer -## Objets à déclarer Il y a donc 2 objets déclarés dans le module : + - `individual` - `marking` -L'objet marking doit être un enfant de l'objet individual. Dans -le fichier `config.json` d'un sous module, il suffit de déclarer +L'objet `marking` doit être un enfant de l'objet `individual`. Dans +le fichier `config.json` d'un sous-module, il suffit de déclarer le `tree` comme suit : ```json @@ -50,18 +51,18 @@ le `tree` comme suit : } ``` -L'objet individual peut-être inséré comme tel pour créer un onglet +L'objet `individual` peut être inséré comme tel pour créer un onglet au même niveau que le site afin d'avoir la liste d'individus facilement accessible. -Il n'est actuellement pas possible de renseigner de champs personnalisés -sur les individus via la création d'un fichier `individual.json` -comme c'est le cas pour les autres objets. +Des champs additionnels personnalisés peuvent être définis au niveau des marquages +des individus via la création d'un fichier `marking.json`. +Il n'est pas possible d'ajouter de champs additionnels au niveau des individus eux-mêmes. Cette impossibilité émane du fait que le [widget individu](#le-widget-individu), créé côté GeoNature, propose un formulaire de création disposant de champs -fixes qui ne doit donc différer du formulaire côté monitoring. +fixes qui ne doit donc pas différer du formulaire côté Monitoring. -## Le widget individu +### Le widget individu Ce widget, disponible dans les composants de GeoNature, permet de : @@ -76,7 +77,6 @@ Et en cliquant sur le "+", un formulaire de création d'individu apparaîtra : ![WidgetCreate](images/individual_widget_create.png) - Il est paramétrable en json comme ceci : ```json @@ -89,21 +89,21 @@ Il est paramétrable en json comme ceci : } ``` -Les attributs sont optionnels si le contraire n'est pas spécifié et sont les suivants : +Les attributs sont optionnels (si le contraire n'est pas spécifié) et sont les suivants : -- `id_module` (**obligatoire**) : permet de spécifier le module auxquel +- `id_module` (**obligatoire**) : permet de spécifier le sous-module auquel doivent être rattachés les individus proposés dans le menu déroulant. Il est obligatoire pour assurer le calcul de permissions de l'utilisateur en "Read" et en "Create". -- `id_list` : dans le formulaire de saisie, restreint la saisie d'espèce à +- `id_list` : dans le formulaire de saisie, restreint la saisie d'espèces à une liste taxonomique - `cd_nom` : fixe le champ Taxon au cd_nom donné et donc ne le fait pas apparaître dans le formulaire. -## Cas du protocole mono-spécifique +### Cas d'un protocole mono-spécifique Il est possible de renseigner une seule espèce pour un protocole. -Comme spécifié dans la documentation du sous module, une variable +Comme spécifié dans la documentation du sous-module, une variable `__MODULE.CD_NOM` est disponible pour renseigner un même `cd_nom` pour chaque widget. @@ -128,7 +128,8 @@ paramétrer un champ `cd_nom` et masquer le champ `id_list_taxonomy` } } ``` -Le fichier observation.json peut donc être écrit de cette manière : + +Le fichier `observation.json` peut donc être écrit de cette manière : ```json { @@ -150,11 +151,10 @@ Le fichier observation.json peut donc être écrit de cette manière : } ``` - -## Cas de l'observation +## Cas des observations Pour que les individus soient implémentés dans le module, la contrainte -`NOT NULL` sur la colonne `cd_nom` de `gn_monitoring.t_observations` a dû +`NOT NULL` sur la colonne `cd_nom` de `gn_monitoring.t_observations` a du être supprimée au profit d'une contrainte `NOT NULL` sur la colonne `cd_nom` **OU** la nouvelle colonne `id_individual`. @@ -184,5 +184,5 @@ Elle permet de désactiver la saisie du `cd_nom` au profit de l'individu. ## Permissions -Comme tout objet monitoring, des permissions seront ajoutées à l'installation +Comme tout objet Monitoring, des permissions seront ajoutées à l'installation pour CRUD sur les objets `MONITORINGS_INDIVIDUALS` et `MONITORINGS_MARKINGS`. From 79c07a54d38d8c2dce67720283036e39a4a6fa7d Mon Sep 17 00:00:00 2001 From: amandine-sahl Date: Tue, 7 Jan 2025 10:00:02 +0100 Subject: [PATCH 10/12] [feat] Add medias to individuals --- backend/gn_module_monitoring/config/generic/individual.json | 5 +++++ backend/gn_module_monitoring/config/generic/marking.json | 5 +++++ backend/gn_module_monitoring/monitoring/schemas.py | 2 -- backend/gn_module_monitoring/monitoring/serializer.py | 1 + dependencies/GeoNature | 2 +- 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/gn_module_monitoring/config/generic/individual.json b/backend/gn_module_monitoring/config/generic/individual.json index c63daf6ed..69352ffc7 100644 --- a/backend/gn_module_monitoring/config/generic/individual.json +++ b/backend/gn_module_monitoring/config/generic/individual.json @@ -70,6 +70,11 @@ "required": true, "hidden": true, "type_util": "user" + }, + "medias": { + "type_widget": "medias", + "attribut_label": "Médias", + "schema_dot_table": "gn_monitoring.t_marking_events" } }, "change": [ diff --git a/backend/gn_module_monitoring/config/generic/marking.json b/backend/gn_module_monitoring/config/generic/marking.json index a958b68d2..2e2c0531b 100644 --- a/backend/gn_module_monitoring/config/generic/marking.json +++ b/backend/gn_module_monitoring/config/generic/marking.json @@ -79,6 +79,11 @@ "required": true, "hidden": true, "type_util": "user" + }, + "medias": { + "type_widget": "medias", + "attribut_label": "Médias", + "schema_dot_table": "gn_monitoring.t_marking_events" } } } \ No newline at end of file diff --git a/backend/gn_module_monitoring/monitoring/schemas.py b/backend/gn_module_monitoring/monitoring/schemas.py index 5bed12fa6..acd45adcf 100644 --- a/backend/gn_module_monitoring/monitoring/schemas.py +++ b/backend/gn_module_monitoring/monitoring/schemas.py @@ -204,5 +204,3 @@ class Meta: model = TMonitoringIndividuals include_fk = True load_relationships = True - - medias = MA.Nested(MediaSchema, many=True) diff --git a/backend/gn_module_monitoring/monitoring/serializer.py b/backend/gn_module_monitoring/monitoring/serializer.py index ccd75d4ee..16efe6e98 100644 --- a/backend/gn_module_monitoring/monitoring/serializer.py +++ b/backend/gn_module_monitoring/monitoring/serializer.py @@ -207,6 +207,7 @@ def serialize(self, depth=1, is_child=False): return None self._model = Model() + # Liste des propriétés de l'objet qui doivent être récupérées display_properties = [] # Liste des propriétés spécifique de l'objet qui doivent être récupérées diff --git a/dependencies/GeoNature b/dependencies/GeoNature index 9576fc376..352f607da 160000 --- a/dependencies/GeoNature +++ b/dependencies/GeoNature @@ -1 +1 @@ -Subproject commit 9576fc3765dc6f01892bba88d6b96817e55f7739 +Subproject commit 352f607da4bacefefa2ec562cb4bd4d10daf5272 From 07e53ac931c093aa02e932869e22fd779252775b Mon Sep 17 00:00:00 2001 From: amandine-sahl Date: Tue, 7 Jan 2025 10:01:24 +0100 Subject: [PATCH 11/12] [fix] Erreur quand aucun item de nomenclature --- backend/gn_module_monitoring/routes/data_utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/gn_module_monitoring/routes/data_utils.py b/backend/gn_module_monitoring/routes/data_utils.py index c1c16dde4..81db1a225 100644 --- a/backend/gn_module_monitoring/routes/data_utils.py +++ b/backend/gn_module_monitoring/routes/data_utils.py @@ -89,9 +89,12 @@ def get_init_data(module_code): for code_type in data.get("nomenclature"): nomenclature_list = get_nomenclature_list(code_type=code_type) # TODO : exception quand pas de valeur - for nomenclature in nomenclature_list["values"]: - nomenclature["code_type"] = code_type - out["nomenclature"].append(nomenclature) + try: + for nomenclature in nomenclature_list["values"]: + nomenclature["code_type"] = code_type + out["nomenclature"].append(nomenclature) + except KeyError: + pass # user if data.get("user"): From 34ab2c3f875d3588c7ac95ec91f2c3ead6616f80 Mon Sep 17 00:00:00 2001 From: amandine-sahl Date: Tue, 7 Jan 2025 16:15:25 +0100 Subject: [PATCH 12/12] =?UTF-8?q?[individual]=20R=C3=A9cup=C3=A9ration=20c?= =?UTF-8?q?d=5Fnom=20via=20trigger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../monitoring/serializer.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/backend/gn_module_monitoring/monitoring/serializer.py b/backend/gn_module_monitoring/monitoring/serializer.py index 16efe6e98..86f0be31e 100644 --- a/backend/gn_module_monitoring/monitoring/serializer.py +++ b/backend/gn_module_monitoring/monitoring/serializer.py @@ -293,24 +293,7 @@ def serialize(self, depth=1, is_child=False): return monitoring_object_dict def preprocess_data(self, data): - - # Query TIndividuals to get the cd_nom - if ( - self._object_type == "observation" - and data["cd_nom"] is None - and data["id_individual"] is not None - or ( - self._object_type == "observation" - and self._id is not None - and data["id_individual"] is not None - ) - ): - individual = TIndividuals.query.get(data["id_individual"]) - if individual is None: - raise ValueError("TIndividuals with provided id_individual not found") - else: - data["cd_nom"] = individual.cd_nom - return data + pass def populate(self, post_data): # pour la partie sur les relationships mettre le from_dict dans utils_flask_sqla ???