Skip to content

Commit

Permalink
Feat Synthese: add status, red lists filters
Browse files Browse the repository at this point in the history
Improve display of form fields used.
Put taxonomic rank fields in a section.
Restore correct order for CorAreaSynthese.
Dynamic tree now avalaible in ngOnInit.
Use "bdc_statut_cor_text_area" name for variable linking tables between
ref_geo and taxonomie.
Use correct field names and css class.
Install Departements before Taxref.

Resolve #1492.
  • Loading branch information
jpm-cbna committed Nov 29, 2022
1 parent c51a7a9 commit fae4c55
Show file tree
Hide file tree
Showing 16 changed files with 680 additions and 237 deletions.
20 changes: 10 additions & 10 deletions backend/geonature/core/gn_synthese/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ def filter_by_scope(self, scope, user=None):
return self


@serializable
class CorAreaSynthese(DB.Model):
__tablename__ = "cor_area_synthese"
__table_args__ = {"schema": "gn_synthese", "extend_existing": True}
id_synthese = DB.Column(
DB.Integer, ForeignKey("gn_synthese.synthese.id_synthese"), primary_key=True
)
id_area = DB.Column(DB.Integer, ForeignKey("ref_geo.l_areas.id_area"), primary_key=True)


@serializable
@geoserializable(geoCol="the_geom_4326", idCol="id_synthese")
@shapeserializable
Expand Down Expand Up @@ -335,16 +345,6 @@ def has_instance_permission(self, scope):
return True


@serializable
class CorAreaSynthese(DB.Model):
__tablename__ = "cor_area_synthese"
__table_args__ = {"schema": "gn_synthese", "extend_existing": True}
id_synthese = DB.Column(
DB.Integer, ForeignKey("gn_synthese.synthese.id_synthese"), primary_key=True
)
id_area = DB.Column(DB.Integer, ForeignKey(LAreas.id_area), primary_key=True)


@serializable
class DefaultsNomenclaturesValue(DB.Model):
__tablename__ = "defaults_nomenclatures_value"
Expand Down
8 changes: 6 additions & 2 deletions backend/geonature/core/gn_synthese/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,13 @@ def get_observations_for_web(info_role):
:param str info_role: Role used to get the associated filters, **TBC**
:qparam str limit: Limit number of synthese returned. Defaults to NB_MAX_OBS_MAP.
:qparam str cd_ref_parent: filtre tous les taxons enfants d'un TAXREF cd_ref.
:qparam str cd_ref: Filter by TAXREF cd_ref attribute
:qparam str taxonomy_group2_inpn: Filter by TAXREF group2_inpn attribute
:qparam str taxonomy_id_hab: Filter by TAXREF id_habitat attribute
:qparam str taxonomy_lr: Filter by TAXREF cd_ref attribute
:qparam str taxhub_attribut*: Generig TAXREF filter, given attribute & value
:qparam str taxhub_attribut*: filtre générique TAXREF en fonction de l'attribut et de la valeur.
:qparam str *_red_lists: filtre générique de listes rouges. Filtre sur les valeurs. Voir config.
:qparam str *_status: filtre générique de statuts (BdC Statuts). Filtre sur les types. Voir config.
:qparam str observers: Filter on observer
:qparam str id_organism: Filter on organism
:qparam str date_min: Start date
Expand Down Expand Up @@ -290,6 +292,7 @@ def get_one_synthese(scope, id_synthese):

if not synthese.has_instance_permission(scope=scope):
raise Forbidden()

geofeature = synthese.as_geofeature(fields=Synthese.nomenclature_fields + fields)
return jsonify(geofeature)

Expand Down Expand Up @@ -440,6 +443,7 @@ def export_observations_web(info_role):
cruved = cruved_scope_for_user_in_module(info_role.id_role, module_code="SYNTHESE")[0]
if cruved["R"] > cruved["E"]:
synthese_query_class.filter_query_with_cruved(info_role)

results = DB.session.execute(
synthese_query_class.query.limit(current_app.config["SYNTHESE"]["NB_MAX_OBS_EXPORT"])
)
Expand Down
102 changes: 101 additions & 1 deletion backend/geonature/core/gn_synthese/utils/query_select_sqla.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import datetime
import uuid

from flask import current_app

from sqlalchemy import func, or_, and_, select
from sqlalchemy.sql import text
from sqlalchemy.orm import aliased
Expand All @@ -16,7 +18,7 @@
from geoalchemy2.shape import from_shape

from geonature.utils.env import DB
from geonature.core.taxonomie.models import Taxref, CorTaxonAttribut, TaxrefLR
from geonature.core.taxonomie.models import TaxrefLR
from geonature.core.gn_synthese.models import (
CorObserverSynthese,
CorAreaSynthese,
Expand All @@ -28,6 +30,15 @@
TDatasets,
)
from geonature.utils.errors import GeonatureApiError
from apptax.taxonomie.models import (
Taxref,
CorTaxonAttribut,
TaxrefBdcStatutTaxon,
bdc_statut_cor_text_area,
TaxrefBdcStatutCorTextValues,
TaxrefBdcStatutText,
TaxrefBdcStatutValues,
)


class SyntheseQuery:
Expand Down Expand Up @@ -339,6 +350,95 @@ def filter_other_filters(self):
if colname.startswith("area"):
self.add_join(CorAreaSynthese, CorAreaSynthese.id_synthese, self.model.id_synthese)
self.query = self.query.where(CorAreaSynthese.id_area.in_(value))
elif colname.endswith("_red_lists"):
red_list_id = colname.replace("_red_lists", "")
all_red_lists_cfg = current_app.config["SYNTHESE"]["RED_LISTS_FILTERS"]
red_list_cfg = next(
(item for item in all_red_lists_cfg if item["id"] == red_list_id), None
)
red_list_cte = (
select([TaxrefBdcStatutTaxon.cd_ref, bdc_statut_cor_text_area.c.id_area])
.select_from(
TaxrefBdcStatutTaxon.__table__.join(
TaxrefBdcStatutCorTextValues,
TaxrefBdcStatutCorTextValues.id_value_text
== TaxrefBdcStatutTaxon.id_value_text,
)
.join(
TaxrefBdcStatutText,
TaxrefBdcStatutText.id_text == TaxrefBdcStatutCorTextValues.id_text,
)
.join(
TaxrefBdcStatutValues,
TaxrefBdcStatutValues.id_value
== TaxrefBdcStatutCorTextValues.id_value,
)
.join(
bdc_statut_cor_text_area,
bdc_statut_cor_text_area.c.id_text == TaxrefBdcStatutText.id_text,
)
)
.where(TaxrefBdcStatutValues.code_statut.in_(value))
.where(TaxrefBdcStatutText.cd_type_statut == red_list_cfg["status_type"])
.where(TaxrefBdcStatutText.enable == True)
.cte(name=f"{red_list_id}_red_list")
)
cas_red_list = aliased(CorAreaSynthese)
self.add_join(cas_red_list, cas_red_list.id_synthese, self.model.id_synthese)
self.add_join(Taxref, Taxref.cd_nom, self.model.cd_nom)
self.add_join_multiple_cond(
red_list_cte,
[
red_list_cte.c.cd_ref == Taxref.cd_ref,
red_list_cte.c.id_area == cas_red_list.id_area,
],
)

elif colname.endswith("_status"):
status_id = colname.replace("_status", "")
all_status_cfg = current_app.config["SYNTHESE"]["STATUS_FILTERS"]
status_cfg = next(
(item for item in all_status_cfg if item["id"] == status_id), None
)
# Check if a checkbox was used.
if (
isinstance(value, list)
and value[0] == True
and len(status_cfg["status_types"]) == 1
):
value = status_cfg["status_types"]
status_cte = (
select([TaxrefBdcStatutTaxon.cd_ref, bdc_statut_cor_text_area.c.id_area])
.select_from(
TaxrefBdcStatutTaxon.__table__.join(
TaxrefBdcStatutCorTextValues,
TaxrefBdcStatutCorTextValues.id_value_text
== TaxrefBdcStatutTaxon.id_value_text,
)
.join(
TaxrefBdcStatutText,
TaxrefBdcStatutText.id_text == TaxrefBdcStatutCorTextValues.id_text,
)
.join(
bdc_statut_cor_text_area,
bdc_statut_cor_text_area.c.id_text == TaxrefBdcStatutText.id_text,
)
)
.where(TaxrefBdcStatutText.cd_type_statut.in_(value))
.where(TaxrefBdcStatutText.enable == True)
.distinct()
.cte(name=f"{status_id}_status")
)
cas_status = aliased(CorAreaSynthese)
self.add_join(cas_status, cas_status.id_synthese, self.model.id_synthese)
self.add_join(Taxref, Taxref.cd_nom, self.model.cd_nom)
self.add_join_multiple_cond(
status_cte,
[
status_cte.c.cd_ref == Taxref.cd_ref,
status_cte.c.id_area == cas_status.id_area,
],
)
elif colname.startswith("id_"):
col = getattr(self.model.__table__.columns, colname)
self.query = self.query.where(col.in_(value))
Expand Down
26 changes: 26 additions & 0 deletions backend/geonature/tests/test_synthese.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,32 @@ def test_get_observations_for_web(self, users, synthese_data, taxon_attribut):
validate_json(instance=r.json, schema=schema)
assert len(r.json["features"]) >= 2

# test status lr
query_string = {"regulations_status": ["REGLLUTTE"]}
r = self.client.get(url, query_string=query_string)
assert r.status_code == 200
# test status znieff
query_string = {"znief_status": True}
r = self.client.get(url, query_string=query_string)
assert r.status_code == 200
# test status protection
query_string = {"protections_status": ["PN"]}
r = self.client.get(url, query_string=query_string)
assert r.status_code == 200
# test LR
query_string = {"worldwide_red_lists": ["LC"]}
r = self.client.get(url, query_string=query_string)
assert r.status_code == 200
query_string = {"european_red_lists": ["LC"]}
r = self.client.get(url, query_string=query_string)
assert r.status_code == 200
query_string = {"national_red_lists": ["LC"]}
r = self.client.get(url, query_string=query_string)
assert r.status_code == 200
query_string = {"regional_red_lists": ["LC"]}
r = self.client.get(url, query_string=query_string)
assert r.status_code == 200

def test_get_synthese_data_cruved(self, app, users, synthese_data, datasets):
set_logged_user_cookie(self.client, users["self_user"])

Expand Down
100 changes: 85 additions & 15 deletions backend/geonature/utils/config_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,76 @@ class GnFrontEndConf(Schema):


class Synthese(Schema):
# --------------------------------------------------------------------
# SYNTHESE - SEARCH FORM
AREA_FILTERS = fields.List(
fields.Dict, load_default=[{"label": "Communes", "type_code": "COM"}]
)
# Nombre de résultat à afficher pour la rechercher autocompleté de taxon
TAXON_RESULT_NUMBER = fields.Integer(load_default=20)
# Afficher ou non l'arbre taxonomique
DISPLAY_TAXON_TREE = fields.Boolean(load_default=True)
# Switch the observer form input in free text input (true) or in select input (false)
SEARCH_OBSERVER_WITH_LIST = fields.Boolean(load_default=False)
# Id of the observer list -- utilisateurs.t_menus
ID_SEARCH_OBSERVER_LIST = fields.Integer(load_default=1)
# Regulatory or not status list of fields
STATUS_FILTERS = fields.List(
fields.Dict,
missing=[
{
"id": "protections",
"show": True,
"display_name": "Taxons protégés",
"status_types": ["PN", "PR", "PD"],
},
{
"id": "regulations",
"show": True,
"display_name": "Taxons réglementés",
"status_types": ["REGLII", "REGLLUTTE", "REGL", "REGLSO"],
},
{
"id": "znief",
"show": True,
"display_name": "Espèces déterminantes ZNIEFF",
"status_types": ["ZDET"],
},
],
)
# Red lists list of fields
RED_LISTS_FILTERS = fields.List(
fields.Dict,
missing=[
{
"id": "worldwide",
"show": True,
"display_name": "Liste rouge mondiale",
"status_type": "LRM",
},
{
"id": "european",
"show": True,
"display_name": "Liste rouge européenne",
"status_type": "LRE",
},
{
"id": "national",
"show": True,
"display_name": "Liste rouge nationale",
"status_type": "LRN",
},
{
"id": "regional",
"show": True,
"display_name": "Liste rouge régionale",
"status_type": "LRR",
},
],
)

# --------------------------------------------------------------------
# SYNTHESE - OBSERVATIONS LIST
# Listes des champs renvoyés par l'API synthese '/synthese'
# Si on veut afficher des champs personnalisés dans le frontend (paramètre LIST_COLUMNS_FRONTEND) il faut
# d'abbord s'assurer que ces champs sont bien renvoyé par l'API !
Expand All @@ -267,6 +334,9 @@ class Synthese(Schema):
)
# Colonnes affichées sur la liste des résultats de la sytnthese
LIST_COLUMNS_FRONTEND = fields.List(fields.Dict, load_default=DEFAULT_LIST_COLUMN)

# --------------------------------------------------------------------
# SYNTHESE - DOWNLOADS (AKA EXPORTS)
EXPORT_COLUMNS = fields.List(fields.String(), load_default=DEFAULT_EXPORT_COLUMNS)
# Certaines colonnes sont obligatoires pour effectuer les filtres CRUVED
EXPORT_ID_SYNTHESE_COL = fields.String(load_default="id_synthese")
Expand All @@ -279,28 +349,28 @@ class Synthese(Schema):
EXPORT_METADATA_ACTOR_COL = fields.String(load_default="acteurs")
# Formats d'export disponibles ["csv", "geojson", "shapefile", "gpkg"]
EXPORT_FORMAT = fields.List(fields.String(), load_default=["csv", "geojson", "shapefile"])
# Nombre de résultat à afficher pour la rechercher autocompleté de taxon
TAXON_RESULT_NUMBER = fields.Integer(load_default=20)
# Nombre max d'observation dans les exports
NB_MAX_OBS_EXPORT = fields.Integer(load_default=50000)

# --------------------------------------------------------------------
# SYNTHESE - OBSERVATION DETAILS
# Liste des id attributs Taxhub à afficher sur la fiche détaile de la synthese
# et sur les filtres taxonomiques avancés
ID_ATTRIBUT_TAXHUB = fields.List(fields.Integer(), load_default=[102, 103])
# nom des colonnes de la table gn_synthese.synthese que l'on veux retirer des filres dynamiques
# et de la modale d'information détaillée d'une observation
# example = "[non_digital_proof]"
# Display email on synthese and validation info obs modal
DISPLAY_EMAIL = fields.Boolean(load_default=True)

# --------------------------------------------------------------------
# SYNTHESE - SHARED PARAMETERS
# Nom des colonnes de la table gn_synthese.synthese que l'on veux retirer des filtres dynamiques
# et de la modale d'information détaillée d'une observation example = "[non_digital_proof]"
EXCLUDED_COLUMNS = fields.List(fields.String(), load_default=[])
# Afficher ou non l'arbre taxonomique
DISPLAY_TAXON_TREE = fields.Boolean(load_default=True)
# Switch the observer form input in free text input (true) or in select input (false)
SEARCH_OBSERVER_WITH_LIST = fields.Boolean(load_default=False)
# id of the observer list -- utilisateurs.t_menus
ID_SEARCH_OBSERVER_LIST = fields.Integer(load_default=1)

# Nombre max d'observation à afficher sur la carte
NB_MAX_OBS_MAP = fields.Integer(load_default=50000)
# clusteriser les layers sur la carte
# Clusteriser les layers sur la carte
ENABLE_LEAFLET_CLUSTER = fields.Boolean(load_default=True)
# Nombre max d'observation dans les exports
NB_MAX_OBS_EXPORT = fields.Integer(load_default=50000)
# Nombre des "dernières observations" affiché à l'arrive sur la synthese
# Nombre des "dernières observations" affichées à l'arrivée sur la synthese
NB_LAST_OBS = fields.Integer(load_default=100)

# Display email on synthese and validation info obs modal
Expand Down
16 changes: 14 additions & 2 deletions config/default_config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,9 @@ MAIL_ON_ERROR = false
ZOOM_ON_CLICK = 16

# Restreindre la recherche OpenStreetMap (sur la carte dans l'encart "Rechercher un lieu")
# à certains pays. Les pays doivent être au format ISO_3166-1 :
# à certains pays. Les pays doivent être au format ISO_3166-1 :
# https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 et séparés par une virgule.
# Exemple : OSM_RESTRICT_COUNTRY_CODES = "fr,es,be,ch" (Restreint à France, Espagne, Belgique
# Exemple : OSM_RESTRICT_COUNTRY_CODES = "fr,es,be,ch" (Restreint à France, Espagne, Belgique
# et Suisse)
# Laisser à null pour n'avoir aucune restriction
OSM_RESTRICT_COUNTRY_CODES = null
Expand Down Expand Up @@ -405,6 +405,18 @@ MAIL_ON_ERROR = false
DISCUSSION_MAX_LENGTH = 1500
ALERT_MODULES = ["SYNTHESE","VALIDATION"]
PIN_MODULES = ["SYNTHESE","VALIDATION"]
RED_LISTS_FILTERS = [
{ "id" = "worldwide", "show" = true, "display_name" = "Liste rouge mondiale", "status_type" = "LRM" },
{ "id" = "european", "show" = true, "display_name" = "Liste rouge européenne", "status_type" = "LRE" },
{ "id" = "national", "show" = true, "display_name" = "Liste rouge nationale", "status_type" = "LRN" },
{ "id" = "regional", "show" = true, "display_name" = "Liste rouge régionale", "status_type" = "LRR" },
]
STATUS_FILTERS = [
{ "id" = "protections", "show" = true, "display_name" = "Taxons protégés", "status_types" = ["PN", "PR", "PD"] },
{ "id" = "regulations", "show" = true, "display_name" = "Taxons réglementés", "status_types" = ["REGLII", "REGL", "REGLSO"] },
{ "id" = "invasive", "show" = true, "display_name" = "Espèces envahissantes", "status_types" = ["REGLLUTTE"] },
{ "id" = "znief", "show" = true, "display_name" = "Espèces déterminantes ZNIEFF", "status_types" = ["ZDET"] },
]

# Configuration de l'accès sans authentication
[PUBLIC_ACCESS]
Expand Down
Loading

0 comments on commit fae4c55

Please sign in to comment.