Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
9ad9432
Refacto : move get_relationship_clause method to parent class
marcantoinedupre Apr 8, 2025
7306835
Refacto : move filter_by_specific method to parent class
marcantoinedupre Apr 8, 2025
c6fc4c5
Refacto : rephrase filter_by_specific method docstring
marcantoinedupre Apr 8, 2025
aeba8fe
Refacto : remove unused **kwargs param of filter_by_specific
marcantoinedupre Apr 8, 2025
79a5e09
Refacto : change get_relationship_clause method into protected static…
marcantoinedupre Apr 8, 2025
82c09df
Refactor list comprehension to improve readability
marcantoinedupre Apr 8, 2025
96f2113
Refacto : remove module_code param of check cruved scope on list endp…
marcantoinedupre Apr 9, 2025
ecf0a66
Refacto : pass current module code to filter_by_readable
marcantoinedupre Apr 10, 2025
71438af
Refacto : add generic fixtures to conftest discovery
marcantoinedupre Apr 10, 2025
5eb9fd9
Ajoute endpoints listes paginées pour Groupes et Sites
marcantoinedupre Apr 8, 2025
bc12544
use sitesgroups and sites components
bastyen Dec 13, 2024
0541435
remove unnecessary request
bastyen Mar 25, 2025
5fecc23
Remove `bEdit: false` in `this._formService.changeFormMapObj` arguments
marcantoinedupre Mar 31, 2025
0476d93
Étend et teste tris sur endpoint liste Sites
marcantoinedupre Apr 11, 2025
cea5e0a
Refactor new tests
marcantoinedupre Apr 22, 2025
a2c7721
fixup! Ajoute endpoints listes paginées pour Groupes et Sites
marcantoinedupre Apr 23, 2025
549cdbe
Ajout #TODO restes à faire backend
marcantoinedupre Apr 28, 2025
e55da47
resolve specific property
bastyen May 28, 2025
77df16f
fix init module
bastyen May 28, 2025
6d7a0cf
fix sites group breadcrumb link
bastyen May 28, 2025
4c69767
prettier write
bastyen May 28, 2025
e0568b8
Rebase with develop
amandine-sahl Jul 23, 2025
17ba60b
[backend] Add route individuals list paginated
amandine-sahl Jul 29, 2025
9b0b9a6
[frontend] Ajout individuals
amandine-sahl Jul 29, 2025
eceda65
Use module permission + factorisation
amandine-sahl Jul 29, 2025
6d14e46
[frontend] fix resolve with multiples values
amandine-sahl Jul 29, 2025
e6a4bce
[frontend] Suppression de getCruvedMonitoring qui récupérait le cruve…
amandine-sahl Jul 29, 2025
ce55ce8
[frontend] Objets site et groupes de site dans le gestionnaire de site
amandine-sahl Jul 29, 2025
d3e226a
[frontend] individual permissions
amandine-sahl Jul 29, 2025
b26238e
[frontend] Load, add, delete individuals
amandine-sahl Jul 30, 2025
03fbcb4
[backend] add route individual DELETE
amandine-sahl Jul 30, 2025
42adab1
[frontend] fix : missing moduleCruved
amandine-sahl Jul 30, 2025
86db438
[Front][Back] Factorisation de la résolution des propriétés et appels…
amandine-sahl Aug 1, 2025
490956c
Use config from service Remove class configJsonService (#464)
amandine-sahl Aug 5, 2025
1b8a1b4
[breadcrumb] Use replaySubject and Backend api (#468)
amandine-sahl Aug 7, 2025
374a4f2
Pytest on pull and push
amandine-sahl Aug 7, 2025
653585e
[backend] Fix order by desc
amandine-sahl Aug 4, 2025
2128258
Add currentModuleConfig subject
amandine-sahl Aug 5, 2025
f7f51cc
wip
amandine-sahl Aug 5, 2025
6336ff4
init _config
amandine-sahl Aug 5, 2025
0897549
Use currentModuleConfigObs
amandine-sahl Aug 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ on:
- main
- hotfixes
- develop
- improve-module-part-indivuduals
pull_request:
paths:
- 'backend/**'
branches:
- main
- hotfixes
- develop
- improve-module-part-indivuduals

jobs:
build:
Expand Down
2 changes: 1 addition & 1 deletion backend/gn_module_monitoring/config/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@ def get_config(module_code=None, force=False):
for t in module.types_site
]
config["default_display_field_names"].update(config.get("display_field_names", {}))
config["display_field_names"] = config["default_display_field_names"]

# preload data # TODO auto from schemas && config recup tax users nomenclatures etc....
config["data"] = get_data_preload(config, module)
Expand All @@ -219,6 +218,7 @@ def get_config(module_code=None, force=False):
config["custom"]["__MODULE.ID_MODULE"] = None
config["custom"]["__MODULE.B_SYNTHESE"] = False

config["display_field_names"] = config["default_display_field_names"]
config["custom"]["__MONITORINGS_PATH"] = get_monitorings_path()
# Remplacement des variables __MODULE.XXX
# par les valeurs spécifiées en base
Expand Down
15 changes: 15 additions & 0 deletions backend/gn_module_monitoring/monitoring/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,21 @@ class TMonitoringSites(TBaseSites, PermissionModel, SitesQuery):

data = DB.Column(JSONB)

modules = DB.relationship(
"TMonitoringModules",
uselist=True, # pourquoi pas par defaut ?
secondaryjoin=lambda: TMonitoringModules.id_module == cor_module_type.c.id_module,
primaryjoin=(id_base_site == cor_site_type.c.id_base_site),
secondary=join(
cor_site_type,
cor_module_type,
cor_site_type.c.id_type_site == cor_module_type.c.id_type_site,
),
foreign_keys=[cor_site_type.c.id_base_site, cor_module_type.c.id_module],
lazy="select",
viewonly=True,
)

visits = DB.relationship(
TMonitoringVisits,
lazy="select",
Expand Down
119 changes: 58 additions & 61 deletions backend/gn_module_monitoring/monitoring/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,75 +91,24 @@ def filter_by_readable(
scope=cls._get_read_scope(module_code=module_code, object_code=object_code, user=user),
)


class SitesQuery(GnMonitoringGenericFilter):
@classmethod
def filter_by_scope(cls, query: Select, scope, user=None):
if user is None:
user = g.current_user
if scope == 0:
query = query.where(false())
elif scope in (1, 2):
ors = [
Models.TMonitoringSites.id_digitiser == user.id_role,
Models.TMonitoringSites.id_inventor == user.id_role,
]
# if organism is None => do not filter on id_organism even if level = 2
if scope == 2 and user.id_organisme is not None:
ors += [
Models.TMonitoringSites.inventor.has(id_organisme=user.id_organisme),
Models.TMonitoringSites.digitiser.has(id_organisme=user.id_organisme),
]
query = query.where(or_(*ors))
return query

@classmethod
def filter_by_params(cls, query: Select, params: MultiDict = None, **kwargs):
if "modules" in params:
query = query.filter(cls.modules.any(id_module=params["modules"]))
params.pop("modules")

if "types_site" in params:
value = params["types_site"]
if not isinstance(value, list):
value = [value]
if value[0].isdigit():
query = query.filter(
cls.types_site.any(Models.BibTypeSite.id_nomenclature_type_site.in_(value))
)
else:
# HACK gestionnaire des sites
# Quand filtre sur type de site envoie une chaine de caractère
params["types_site_label"] = value[0]
if "types_site_label" in params:
value = params["types_site_label"]
join_types_site = aliased(Models.BibTypeSite)
join_nomenclature_type_site = aliased(TNomenclatures)
query = query.join(join_types_site, cls.types_site)
query = query.join(join_nomenclature_type_site, join_types_site.nomenclature)
query = query.filter(join_nomenclature_type_site.label_default.ilike(f"%{value}%"))

query = super().filter_by_params(query, params)
return query

@classmethod
def filter_by_specific(
cls,
query: Select,
params: MultiDict = None,
specific_properties: dict = None,
**kwargs,
):
"""
Permet d'ajouter des filtres à la requête des sites
en fonction des propriétés spécifiques définies au niveau du module ou des types de sites
Permet d'ajouter les filtres définis dans `params` à la requête SQLA `query`. Les filtres ciblent les propriétés
spécifiques enregistrées dans le champ JSON `data` du modèle. Les définitions de ces propriétés sont attendues dans
`specific_properties` telles que présentes dans les configs.

le principe est pour chaque params (c-a-d filtre) d'extraire le type util et la cardinalité
et de construire une requête sql en fonction de ces infos

:param query: requête sql initiale
:param params: liste des paramètres que l'on souhaite filtrer
:param specific_properties: Configuration des propriétés spécifiques des sites
:param specific_properties: Configuration des propriétés spécifiques
:return: requête sql amendée de filtre
"""
for param, value in params.items():
Expand All @@ -176,7 +125,7 @@ def filter_by_specific(
multiple = json.loads(multiple_value)

if type in ("nomenclature", "taxonomy", "user", "area"):
join_table, join_column, filter_column = cls.get_relationship_clause(type)
join_table, join_column, filter_column = cls._get_relationship_clause(type)
if multiple:
# Si la propriété est de type multiple
# Alors jointure sur chaque element de data->'params'
Expand Down Expand Up @@ -212,11 +161,8 @@ def filter_by_specific(

return query

@classmethod
def get_relationship_clause(
cls,
type,
):
@staticmethod
def _get_relationship_clause(type):
join_table = None # alias de la table de jointure
join_column = None # nom de la colonne permettant la jointure entre data et la table
filter_column = None # nom de la colonne sur lequel le filtre est appliqué
Expand All @@ -242,6 +188,57 @@ def get_relationship_clause(
return join_table, join_column, filter_column


class SitesQuery(GnMonitoringGenericFilter):
@classmethod
def filter_by_scope(cls, query: Select, scope, user=None):
if user is None:
user = g.current_user
if scope == 0:
query = query.where(false())
elif scope in (1, 2):
ors = [
Models.TMonitoringSites.id_digitiser == user.id_role,
Models.TMonitoringSites.id_inventor == user.id_role,
]
# if organism is None => do not filter on id_organism even if level = 2
if scope == 2 and user.id_organisme is not None:
ors += [
Models.TMonitoringSites.inventor.has(id_organisme=user.id_organisme),
Models.TMonitoringSites.digitiser.has(id_organisme=user.id_organisme),
]
query = query.where(or_(*ors))
return query

@classmethod
def filter_by_params(cls, query: Select, params: MultiDict = None, **kwargs):
if "modules" in params:
query = query.filter(cls.modules.any(id_module=params["modules"]))
params.pop("modules")

if "types_site" in params:
value = params["types_site"]
if not isinstance(value, list):
value = [value]
if value[0].isdigit():
query = query.filter(
cls.types_site.any(Models.BibTypeSite.id_nomenclature_type_site.in_(value))
)
else:
# HACK gestionnaire des sites
# Quand filtre sur type de site envoie une chaine de caractère
params["types_site_label"] = value[0]
if "types_site_label" in params:
value = params["types_site_label"]
join_types_site = aliased(Models.BibTypeSite)
join_nomenclature_type_site = aliased(TNomenclatures)
query = query.join(join_types_site, cls.types_site)
query = query.join(join_nomenclature_type_site, join_types_site.nomenclature)
query = query.filter(join_nomenclature_type_site.label_default.ilike(f"%{value}%"))

query = super().filter_by_params(query, params)
return query


class SitesGroupsQuery(GnMonitoringGenericFilter):
@classmethod
def filter_by_scope(cls, query: Select, scope, user=None):
Expand Down
23 changes: 18 additions & 5 deletions backend/gn_module_monitoring/monitoring/repositories.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from flask import current_app
from flask import current_app, g

from sqlalchemy import select
from sqlalchemy.sql import text
Expand Down Expand Up @@ -189,13 +189,24 @@ def breadcrumbs(self, params):
breadcrumbs = [breadcrumb] if breadcrumb else []

next = None

next_breadcrumbs = None
if params["parents_path"]:
object_type = params.get("parents_path", []).pop()
next = MonitoringObject(self._module_code, object_type, config=self._config)
if next._object_type == "module":
next.get(field_name="module_code", value=self._module_code)
if object_type == "module":
next = None
if g.current_module.module_code.upper() == "MONITORINGS":
module_code = "generic"
else:
module_code = g.current_module.module_code
next_breadcrumbs = {
"description": g.current_module.module_label,
"id": g.current_module.id_module,
"label": "Module",
"module_code": module_code,
"object_type": "module",
}
else:
next = MonitoringObject(self._module_code, object_type, config=self._config)
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)
Expand All @@ -204,6 +215,8 @@ def breadcrumbs(self, params):

if next:
breadcrumbs = next.breadcrumbs(params) + breadcrumbs
if next_breadcrumbs:
breadcrumbs = [next_breadcrumbs] + breadcrumbs

return breadcrumbs

Expand Down
55 changes: 46 additions & 9 deletions backend/gn_module_monitoring/monitoring/schemas.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import json
import geojson

from flask import g
from marshmallow import Schema, fields, validate, post_dump
import marshmallow

from geonature.utils.env import MA
from geonature.core.gn_commons.schemas import MediaSchema, ModuleSchema
Expand Down Expand Up @@ -32,6 +34,45 @@ class PaginationSchema(Schema):
return PaginationSchema


def add_specific_attributes(schema, object_type, module_code):
"""Crée une classe Schema dynamiquement pour ajouter les propriétés spécifiques du type d'objet
à la classe 'schema' passée en argument."""

# FIXME: déplacer ces imports hors de la fonction mais il faut résoudre un pb de circular import
from gn_module_monitoring.config.repositories import get_config
from gn_module_monitoring.monitoring.definitions import (
MonitoringModels_dict,
MonitoringObjects_dict,
)
from gn_module_monitoring.monitoring.geom import MonitoringObjectGeom

config = get_config(module_code, force=True)

specific_properties = config[object_type]["specific"]

def create_getter(key):
return lambda obj: (obj.data or {}).get(key)

attrs = {}
for k, v in specific_properties.items():
attrs[k] = marshmallow.fields.Function(create_getter(k))

monitoring_object_class = MonitoringObjects_dict[object_type]
model_class = MonitoringModels_dict[object_type]
parameters = {"model": model_class, "exclude": ["data"], "include_fk": True}
if issubclass(monitoring_object_class, MonitoringObjectGeom):
parameters["exclude"].extend(["geom_geojson", "geom"])
Meta = type("Meta", (), parameters)

attrs.update({"Meta": Meta})
schema_with_specifics = type(
f"{object_type.capitalize()}SchemaWithSpecifics",
(schema,),
attrs,
)
return schema_with_specifics


class ObserverSchema(MA.SQLAlchemyAutoSchema):
class Meta:
model = User
Expand Down Expand Up @@ -112,10 +153,6 @@ def serialize_geojson(self, obj):
return json.loads(obj.geom_geojson)


class MonitoringSitesGroupsDetailSchema(MonitoringSitesGroupsSchema):
modules = MA.Pluck(ModuleSchema, "module_label", many=True)


class BibTypeSiteSchema(MA.SQLAlchemyAutoSchema):
label = fields.Method("get_label_from_type_site")
# See if useful in the future:
Expand All @@ -142,7 +179,6 @@ class Meta:
types_site = MA.Nested(BibTypeSiteSchema, many=True)
id_sites_group = fields.Method("get_id_sites_group")
id_inventor = fields.Method("get_id_inventor")
inventor = fields.Method("get_inventor_name")
medias = MA.Nested(MediaSchema, many=True)
nb_visits = fields.Integer(dump_only=True)
last_visit = fields.DateTime(dump_only=True)
Expand All @@ -160,10 +196,6 @@ def get_id_sites_group(self, obj):
def get_id_inventor(self, obj):
return obj.id_inventor

def get_inventor_name(self, obj):
if obj.inventor:
return [obj.inventor.nom_complet]


class MonitoringVisitsSchema(MA.SQLAlchemyAutoSchema):
class Meta:
Expand Down Expand Up @@ -206,3 +238,8 @@ class Meta:
load_relationships = True

medias = MA.Nested(MediaSchema, many=True)

pk = fields.Method("set_pk", dump_only=True)

def set_pk(self, obj):
return "id_individual"
18 changes: 10 additions & 8 deletions backend/gn_module_monitoring/monitoring/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,14 +276,16 @@ def serialize(self, depth=1, is_child=False, scope=None):
# on passe d'une list d'objet à une liste d'id
# si type_util est defini pour ce champs
# si on a bien affaire à une liste de modèles sqla
properties[key] = [
(
getattr(v, id_field_name_dict[type_util])
if (isinstance(v, DB.Model) and type_util)
else v.as_dict() if (isinstance(v, DB.Model) and not type_util) else v
)
for v in value
]
new_values = []
for v in value:
if isinstance(v, DB.Model):
if type_util:
new_values.append(getattr(v, id_field_name_dict[type_util]))
else:
new_values.append(v.as_dict())
else:
new_values.append(v)
properties[key] = new_values

properties["id_parent"] = to_int(self.id_parent())

Expand Down
Loading