Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 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
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
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
44 changes: 44 additions & 0 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,48 @@ 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"],
}
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
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
63 changes: 56 additions & 7 deletions backend/gn_module_monitoring/routes/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
cor_module_type,
cor_site_type,
)
from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema
from gn_module_monitoring.monitoring.schemas import (
BibTypeSiteSchema,
MonitoringSitesSchema,
add_specific_attributes,
)
from gn_module_monitoring.routes.modules import get_modules
from gn_module_monitoring.routes.monitoring import (
create_or_update_object_api,
Expand Down Expand Up @@ -112,24 +116,51 @@ def get_all_types_site_from_site_id(id_site, object_type):


@blueprint.route("/sites", methods=["GET"], defaults={"object_type": "site"})
@check_cruved_scope("R", module_code=MODULE_CODE, object_code="MONITORINGS_SITES")
def get_sites(object_type):
@blueprint.route(
"/refacto/<string:module_code>/sites", methods=["GET"], defaults={"object_type": "site"}
)
@check_cruved_scope("R", object_code="MONITORINGS_SITES")
def get_sites(object_type, module_code=None):
object_code = "MONITORINGS_SITES"
params = MultiDict(request.args)
# TODO: add filter support
limit, page = get_limit_page(params=params)
sort_label, sort_dir = get_sort(
params=params, default_sort="id_base_site", default_direction="desc"
)

query = select(TMonitoringSites)

if module_code:
query = query.where(
TMonitoringSites.modules.any(TMonitoringModules.module_code == module_code)
)

config = get_config(g.current_module.module_code)
specific_properties = config.get("site", {}).get("specific", {})

query = filter_params(TMonitoringSites, query=query, params=params)
query = sort_according_to_column_type_for_site(query, sort_label, sort_dir)
query = sort_according_to_column_type_for_site(
query, sort_label, sort_dir, specific_properties
)

query_allowed = TMonitoringSites.filter_by_readable(
query=query, object_code=object_code, module_code=g.current_module.module_code
)

query_allowed = TMonitoringSites.filter_by_specific(
query=query_allowed,
params=params,
specific_properties=specific_properties,
)

if module_code:
schema = add_specific_attributes(MonitoringSitesSchema, object_type, module_code)
else:
schema = MonitoringSitesSchema

query_allowed = TMonitoringSites.filter_by_readable(query=query, object_code=object_code)
return paginate_scope(
query=query_allowed,
schema=MonitoringSitesSchema,
schema=schema,
limit=limit,
page=page,
object_code=object_code,
Expand All @@ -156,6 +187,20 @@ def get_site_by_id(scope, id, object_type):
@blueprint.route("/sites/geometries", methods=["GET"], defaults={"object_type": "site"})
@check_cruved_scope("R")
def get_all_site_geometries(object_type):
return _get_site_geometries()


@blueprint.route(
"/refacto/<string:module_code>/sites/geometries",
methods=["GET"],
defaults={"object_type": "site"},
)
@check_cruved_scope("R")
def get_module_site_geometries(object_type, module_code):
return _get_site_geometries(module_code)


def _get_site_geometries(module_code=None):
object_code = "MONITORINGS_SITES"
# params = request.args.to_dict(flat=True)
params = dict(**request.args)
Expand All @@ -180,6 +225,10 @@ def get_all_site_geometries(object_type):
query_allowed = TMonitoringSites.filter_by_readable(
query=query, module_code=module_code, object_code=object_code
)
if module_code != MODULE_CODE:
query_allowed = query_allowed.where(
TMonitoringSites.modules.any(TMonitoringModules.module_code == module_code)
)
query_allowed = query_allowed.with_only_columns(
TMonitoringSites.id_base_site,
TMonitoringSites.base_site_name,
Expand Down
Loading