From c92c4967769d5bf5c6e0a3c3205bbe2657d84947 Mon Sep 17 00:00:00 2001 From: jacquesfize Date: Thu, 6 Feb 2025 16:20:53 +0100 Subject: [PATCH] wip --- backend/gn_module_dashboard/blueprint.py | 515 +++++++++++++++-------- 1 file changed, 333 insertions(+), 182 deletions(-) diff --git a/backend/gn_module_dashboard/blueprint.py b/backend/gn_module_dashboard/blueprint.py index 124fff5..376fbe9 100644 --- a/backend/gn_module_dashboard/blueprint.py +++ b/backend/gn_module_dashboard/blueprint.py @@ -9,11 +9,14 @@ from werkzeug.exceptions import BadRequest from utils_flask_sqla.response import json_resp -from geonature.utils.env import DB, db +from geonature.utils.env import db from .models import VSynthese, VTaxonomie, VFrameworks from geonature.core.gn_synthese.models import Synthese, CorAreaSynthese from ref_geo.models import BibAreasTypes, LAreas +from ref_geo.schemas import AreaTypeSchema +from apptax.taxonomie.models import Taxref +from geonature.core.gn_meta.models import TDatasets # # import des fonctions utiles depuis le sous-module d'authentification # from geonature.core.gn_permissions import decorators as permissions @@ -27,6 +30,39 @@ @blueprint.route("/synthese", methods=["GET"]) @json_resp def get_synthese_stat(): + """ + Retourne le nombre d'observations et le nombre de taxons pour chaque année. + + Parameters + ---------- + selectedRegne : string + Règne taxonomique + selectedPhylum : string + Phylum taxonomique + selectedClasse : string + Classe taxonomique + selectedOrdre : string + Ordre taxonomique + selectedFamille : string + Famille taxonomique + selectedGroup1INPN : string + Groupe 1 INPN + selectedGroup2INPN : string + Groupe 2 INPN + selectedGroup3INPN : string + Groupe 3 INPN + taxon : string + Code du taxon + + Returns + ------- + year : int + Année + count_id_synthese : integer + Nombre d'observations + count_cd_ref : int + Nombre de taxons + """ params = request.args query = sa.select( func.date_part("year", VSynthese.date_min).label("year"), @@ -48,7 +84,7 @@ def get_synthese_stat(): for param, column in filters.items(): if param in params and params[param] != "": - query = query.filter(column == params[param]) + query = query.where(column == params[param]) return db.session.execute(query).all() @@ -56,10 +92,28 @@ def get_synthese_stat(): @blueprint.route("/areas//", methods=["GET"]) @json_resp def get_areas_stat(simplify_level, type_code): + """ + Retourne le nombre d'observations et le nombre de taxons pour chaque zone + avec une échelle donnée (type_code) et un niveau de simplification donnée + (simplify_level). + + Parameters + ---------- + simplify_level : int + Niveau de simplification de la géométrie des zones. + type_code : string + Code de l'échelle souhaitée. + + Returns + ------- + geojson : FeatureCollection + Une collection de Feature au format geojson contenant le nombre + d'observations et le nombre de taxons pour chaque zone. + """ params = request.args - # x : Variable contenant les conditions WHERE à ajouter à la requête générale - year_start = request.args.get("yearStart", None) - year_end = request.args.get("yearEnd", None) + + year_start = params.get("yearStart", None) + year_end = params.get("yearEnd", None) where_clause = [] if year_start: @@ -112,35 +166,47 @@ def get_areas_stat(simplify_level, type_code): count_cte.c.nb_obs, count_cte.c.nb_tax, ).join(count_cte, count_cte.c.id_area == LAreas.id_area) - # q : Requête générale - data = db.session.execute(query) + response = db.session.execute(query) geojson_features = [] - for elt in data: - geojson = json.loads(elt[1]) - properties = { - "area_name": elt[0], - "nb_obs": int(elt[2]), - "nb_taxons": int(elt[3]), + for area_name, geojson_, nb_obs, nb_tax in response: + geojson = json.loads(geojson_) + geojson["properties"] = { + "area_name": area_name, + "nb_obs": int(nb_obs), + "nb_taxons": int(nb_tax), } - geojson["properties"] = properties geojson_features.append(geojson) return FeatureCollection(geojson_features) -# Obtenir le nombre d'observations pour chaque taxon avec un rang taxonomique donné -# vm_synthese @blueprint.route("/synthese_per_tax_level/", methods=["GET"]) @json_resp def get_synthese_per_tax_level_stat(taxLevel): + """ + Obtenir le nombre d'observations pour chaque taxon avec un rang taxonomique donné + + Parameters + ---------- + taxLevel : str + rang taxonomique + + Returns + ------- + List of dictionaries + taxon_name : str + nom du taxon + nb_obs : int + nombre d'observations + """ params = request.args try: column_taxlevel = getattr(VSynthese, taxLevel) except AttributeError: raise BadRequest(f"No attribute {taxLevel} in VSynthese VM") - q = ( - DB.session.query( + query = ( + sa.select( column_taxlevel, func.count(VSynthese.id_synthese), ) @@ -148,18 +214,39 @@ def get_synthese_per_tax_level_stat(taxLevel): .order_by(column_taxlevel) ) if "yearStart" in params and "yearEnd" in params: - q = q.filter(func.date_part("year", VSynthese.date_min) >= params["yearStart"]) - q = q.filter(func.date_part("year", VSynthese.date_max) <= params["yearEnd"]) - return [{"taxon": d[0], "nb_obs": d[1]} for d in q.all()] + query = query.where(func.date_part("year", VSynthese.date_min) >= params["yearStart"]) + query = query.where(func.date_part("year", VSynthese.date_max) <= params["yearEnd"]) + return [ + {"taxon": taxon, "nb_obs": nb_obs} + for (taxon, nb_obs) in db.session.execute(query).all() # TODO rename taxon to taxonLevel? + ] -# Obtenir le nombre d'observations par cadre d'acquisition par année -# vm_synthese_frameworks @blueprint.route("/frameworks", methods=["GET"]) @json_resp def get_frameworks_stat(): + """ + Obtenir le nombre d'observations par cadre d'acquisition par année + vm_synthese_frameworks + + Parameters + ---------- + id_acquisition_framework : list of int + liste des id des cadres d'acquisitions à filtre + + Returns + ------- + list[dict] + acquisition_framework_name : str + nom du cadre d'acquisition + data : list of dictionaries + year : int + année + nb_obs : int + nombre d'observations + """ afs = request.args.getlist("id_acquisition_framework") - q = DB.session.query( + query = sa.select( func.json_build_object( "acquisition_framework_name", VFrameworks.acquisition_framework_name, @@ -170,197 +257,261 @@ def get_frameworks_stat(): ) ).group_by(VFrameworks.acquisition_framework_name) if afs: - q = q.filter(VFrameworks.id_acquisition_framework.in_(afs)) - q = q.order_by(VFrameworks.acquisition_framework_name) - return [d[0] for d in q.all()] + query = query.where(VFrameworks.id_acquisition_framework.in_(afs)) + query = query.order_by(VFrameworks.acquisition_framework_name) + return [d[0] for d in db.session.execute(query).all()] -# Obtenir le nombre de taxons recontactés, non recontactés et nouveaux pour une année donnée @blueprint.route("/recontact/", methods=["GET"]) @json_resp def get_recontact_stat(year): - q = text( - """ WITH recontactees AS - (SELECT DISTINCT cd_ref FROM gn_synthese.synthese s JOIN taxonomie.taxref t ON t.cd_nom=s.cd_nom WHERE date_part('year', date_min) < :selectedYear - INTERSECT - SELECT DISTINCT cd_ref FROM gn_synthese.synthese s JOIN taxonomie.taxref t ON t.cd_nom=s.cd_nom WHERE date_part('year', date_min) = :selectedYear), - non_recontactees AS - (SELECT DISTINCT cd_ref FROM gn_synthese.synthese s JOIN taxonomie.taxref t ON t.cd_nom=s.cd_nom WHERE date_part('year', date_min) < :selectedYear - EXCEPT - SELECT DISTINCT cd_ref FROM gn_synthese.synthese s JOIN taxonomie.taxref t ON t.cd_nom=s.cd_nom WHERE date_part('year', date_min) = :selectedYear), - nouvelles AS - (SELECT DISTINCT cd_ref FROM gn_synthese.synthese s JOIN taxonomie.taxref t ON t.cd_nom=s.cd_nom WHERE date_part('year', date_min) = :selectedYear - EXCEPT - SELECT DISTINCT cd_ref FROM gn_synthese.synthese s JOIN taxonomie.taxref t ON t.cd_nom=s.cd_nom WHERE date_part('year', date_min) < :selectedYear) - - SELECT count(cd_ref) FROM recontactees - UNION ALL - SELECT count(cd_ref) FROM non_recontactees - UNION ALL - SELECT count(cd_ref) FROM nouvelles """ + """ + Obtenir le nombre de taxons recontactés, non recontactés et nouveaux pour une année donnée. + + Parameters + ---------- + year : int + Année pour laquelle les statistiques de recontact doivent être calculées. + + Returns + ------- + List[int] + Une liste contenant trois entiers : + - Le nombre de taxons recontactés (observés à la fois l'année spécifiée et les années précédentes). + - Le nombre de taxons non recontactés (observés uniquement les années précédentes). + - Le nombre de nouveaux taxons (observés uniquement l'année spécifiée). + """ + + cd_ref_actual_year = ( + sa.select(func.distinct(Taxref.cd_ref)) + .select_from(Synthese) + .join(Taxref, Taxref.cd_nom == Synthese.cd_nom) + .where(sa.func.date_part("year", Synthese.date_min) == year) + ) + cd_ref_year_before = ( + sa.select(func.distinct(Taxref.cd_ref)) + .select_from(Synthese) + .join(Taxref, Taxref.cd_nom == Synthese.cd_nom) + .where(sa.func.date_part("year", Synthese.date_min) < year) ) - data = DB.engine.execute(q, selectedYear=year) + + recontactees_query = sa.intersect(cd_ref_year_before, cd_ref_actual_year) + non_recontactees_query = sa.except_(cd_ref_year_before, cd_ref_actual_year) + nouvelles_query = sa.except_(cd_ref_actual_year, cd_ref_year_before) + + query = sa.union_all( + select(func.count()).select_from(recontactees_query.subquery()), + select(func.count()).select_from(non_recontactees_query.subquery()), + select(func.count()).select_from(nouvelles_query.subquery()), + ) + + data = db.session.execute(query).all() return [elt[0] for elt in data] -# Obtenir la liste des taxons observés pour un rang taxonomique donné -# vm_taxonomie @blueprint.route("/taxonomy/", methods=["GET"]) @json_resp def get_taxonomy(taxLevel): - q = ( - DB.session.query(VTaxonomie.name_taxon) + """ + Retourne la liste des taxons observés pour un rang taxonomique donné. + + Parameters + ---------- + taxLevel : string + Le rang taxonomique souhaité (par exemple, 'Règne', 'Famille', etc.). + + Returns + ------- + list + Une liste de noms de taxons. + """ + query = ( + sa.select(VTaxonomie.name_taxon) .order_by( case([(VTaxonomie.name_taxon == "Not defined", 1)], else_=0), VTaxonomie.name_taxon, ) - .filter(VTaxonomie.level == taxLevel) + .where(VTaxonomie.level == taxLevel) ).order_by(VTaxonomie.name_taxon) - return q.all() + return db.session.execute(query).all() -# Obtenir la liste des type_name des areas_types @blueprint.route("/areas_types", methods=["GET"]) @json_resp def get_areas_types(): - params = request.args - q = DB.session.query(BibAreasTypes) - if "type_code" in params: - tab_types_codes = params.getlist("type_code") - q = q.filter(BibAreasTypes.type_code.in_(tab_types_codes)) - data = q.all() - return [elt.as_dict() for elt in data] + """ + Retourne la liste des types de zonages. + + Parameters + ---------- + type_code : string (optional) + Si fourni, filtre les résultats pour ne garder que les types de zonages + portant ce code. + + Returns + ------- + list + Une liste de dictionnaires, chaque dictionnaire contenant les clés + `type_code` et `type_name` pour chaque type de zonage. + """ + query = sa.select(BibAreasTypes) + if "type_code" in request.args: + tab_types_codes = request.args.getlist("type_code") + query = query.where(BibAreasTypes.type_code.in_(tab_types_codes)) + + return jsonify( + AreaTypeSchema(many=True).dump( + db.session.scalars(query).unique().all(), + ) + ) -# Obtenir la liste des années au cours desquelles des observations ont été faîtes -# OU obtenir l'année min et l'année max de cette liste -# vm_synthese @blueprint.route("/years", methods=["GET"]) @json_resp def get_years(): - q = DB.session.query( - label("year", distinct(func.date_part("year", VSynthese.date_min))) - ).order_by("year") - return [d[0] for d in q.all()] + """ + Renvoie la liste des années distinctes pour lesquelles des observations + ont été faites. + + Returns + ------- + list[int] + Une liste triée des années uniques extraites du champ 'date_min' + de la vue VSynthese. + """ + + query = sa.select( + func.distinct( + sa.cast( + func.date_part( + "year", + VSynthese.date_min, + ), + sa.Integer, + ), + ).label("year") + ).order_by(text("year")) + return db.session.scalars(query).all() @blueprint.route("/report/", methods=["GET"]) def yearly_recap(year): - nb_obs_year = DB.session.execute( - """ - SELECT count(*) - FROM gn_synthese.synthese - WHERE date_part('year', date_min) = :year - """, - {"year": year}, - ).scalar() - nb_obs_total = DB.session.execute( - """ - SELECT count(*) - FROM gn_synthese.synthese - WHERE date_part('year', date_min) <= :year - """, - {"year": year}, - ).scalar() - nb_new_species = DB.session.execute( - """ - SELECT COUNT(*) FROM ( - SELECT DISTINCT t.cd_ref - FROM gn_synthese.synthese s JOIN taxonomie.taxref t ON t.cd_nom=s.cd_nom - WHERE date_part('year', date_min) = :year - EXCEPT - SELECT DISTINCT t.cd_ref - FROM gn_synthese.synthese s JOIN taxonomie.taxref t ON t.cd_nom=s.cd_nom - WHERE date_part('year', date_min) < :year - ) sub - """, - {"year": year}, - ).scalar() - new_datasets = DB.session.execute( - """ - SELECT count(*) - FROM gn_meta.t_datasets td - WHERE date_part('year', td.meta_create_date) = :year - """, - {"year": year}, - ).scalar() - new_species = DB.session.execute( - """ - SELECT t.nom_complet, t.nom_vern, t.group2_inpn, count(s.*) FROM ( - SELECT DISTINCT t.cd_ref - FROM gn_synthese.synthese s JOIN taxonomie.taxref t ON t.cd_nom=s.cd_nom - WHERE date_part('year', date_min) = :year - EXCEPT - SELECT DISTINCT t.cd_ref - FROM gn_synthese.synthese s JOIN taxonomie.taxref t ON t.cd_nom=s.cd_nom - WHERE date_part('year', date_min) < :year - ) sub - JOIN gn_synthese.synthese s ON sub.cd_ref = taxonomie.find_cdref(s.cd_nom) - JOIN taxonomie.taxref t on t.cd_nom = sub.cd_ref - WHERE date_part('year', date_min) = :year - GROUP BY t.nom_vern, t.nom_complet, t.group2_inpn - ORDER BY t.nom_complet ASC - """, - {"year": year}, - ).fetchall() - most_viewed_species = DB.session.execute( - """ - SELECT t.nom_complet, t.nom_vern, t.group2_inpn, count(*) - FROM gn_synthese.synthese s - JOIN taxonomie.taxref t on t.cd_nom = s.cd_nom - WHERE date_part('year', date_min) = :year - GROUP BY t.nom_complet , t.nom_vern, t.group2_inpn - ORDER BY count(*) desc - LIMIT 10 - """, - {"year": year}, - ).fetchall() + """ + Renvoie un objet JSON contenant les informations suivantes pour une + année donnée : + - yearsWithObs: une liste des années pour lesquelles des observations + ont été faites + - year: l'année demandée + - nb_obs_year: le nombre d'observations pour l'année demandée + - nb_obs_total: le nombre total d'observations + - nb_new_species: le nombre de nouvelles espèces observées + - new_datasets: le nombre de nouveaux jeux de données + - new_species: une liste des nouvelles espèces observées + - most_viewed_species: une liste des 10 espèces les plus vues + - observations_by_group: une liste du nombre d'observations par groupe + - data_by_datasets: une liste du nombre d'observations par jeu de données + - observations_by_year: une liste du nombre d'observations par année + """ + nb_obs_year = db.session.scalar( + sa.select(sa.func.count()) + .select_from(VSynthese) + .where(sa.func.date_part("year", VSynthese.date_min) == year) + ) - data_by_datasets = DB.session.execute( - """ - SELECT td.dataset_name, count(*) - FROM gn_synthese.synthese s - JOIN gn_meta.t_datasets td on s.id_dataset = td.id_dataset - WHERE date_part('year', s.date_min) = :year - GROUP BY td.dataset_name - ORDER BY count(*) desc - """, - {"year": year}, - ).fetchall() - nb_taxon_year = DB.session.execute( - """ - SELECT count(distinct cd_ref) - FROM gn_synthese.synthese s - JOIN taxonomie.taxref t on s.cd_nom = t.cd_nom - WHERE date_part('year', s.date_min) = :year - """, - {"year": year}, + nb_obs_total = db.session.scalar( + sa.select(sa.func.count()) + .select_from(VSynthese) + .where(sa.func.date_part("year", VSynthese.date_min) <= year) + ) + + new_species_query = sa.except_( + sa.select(VSynthese.cd_ref) + .join(Taxref, Taxref.cd_nom == VSynthese.cd_nom) + .where(sa.func.date_part("year", VSynthese.date_min) == year), + sa.select(VSynthese.cd_ref) + .join(Taxref, Taxref.cd_nom == VSynthese.cd_nom) + .where(sa.func.date_part("year", VSynthese.date_min) < year), + ) + new_species_cte = new_species_query.cte() + + nb_new_species = db.session.scalar( + sa.select(sa.func.count()).select_from(new_species_query.subquery()) + ) + + new_datasets = db.session.scalar( + sa.select(func.count()).where( + func.date_part("year", TDatasets.meta_create_date) == year, + ) + ) + new_species = ( + sa.select( + Taxref.nom_complet, + Taxref.nom_vern, + Taxref.group2_inpn, + func.count(VSynthese.id_synthese), + ) + .select_from(new_species_cte) + .join(VSynthese, VSynthese.cd_ref == new_species_cte.c.cd_ref) + .join(Taxref, Taxref.cd_ref == new_species_cte.c.cd_ref) + .where(func.date_part("year", VSynthese.date_min) == year) + .group_by(Taxref.nom_complet, Taxref.nom_vern, Taxref.group2_inpn) + .order_by( + Taxref.nom_complet, + ) + ) + new_species = db.session.execute(new_species).all() + + most_viewed_species_query = ( + sa.select( + Taxref.nom_complet, Taxref.nom_vern, Taxref.group2_inpn, func.count().label("count") + ) + .join(VSynthese, Taxref.cd_nom == VSynthese.cd_nom) + .where(func.date_part("year", VSynthese.date_min) == year) + .group_by(Taxref.nom_complet, Taxref.nom_vern, Taxref.group2_inpn) + .order_by(sa.desc("count")) + .limit(10) + ) + most_viewed_species = db.session.execute(most_viewed_species_query).fetchall() + + data_by_datasets = db.session.execute( + sa.select(TDatasets.dataset_name, sa.func.count()) + .join(VSynthese, VSynthese.id_dataset == TDatasets.id_dataset) + .where(func.date_part("year", VSynthese.date_min) == year) + .group_by(TDatasets.dataset_name) + .order_by(sa.desc(sa.func.count())) + ).all() + + nb_taxon_year = db.session.execute( + sa.select(sa.func.count(sa.distinct(VSynthese.cd_ref))) + .join(Taxref, Taxref.cd_nom == VSynthese.cd_nom) + .where(sa.func.date_part("year", VSynthese.date_min) == year) ).scalar() - observations_by_year = DB.session.execute( - """ - select count(id_synthese), date_part('year', s.date_min) as year_ - from gn_synthese.synthese s - WHERE date_part('year', s.date_min) >= 1990 - group by year_ - order by year_ ASC - """ + observations_by_year = db.session.execute( + sa.select( + sa.func.count(VSynthese.id_synthese), + sa.cast(sa.func.date_part("year", VSynthese.date_min), sa.Integer).label("year_"), + ) + .where(sa.func.date_part("year", VSynthese.date_min) >= 1990) + .group_by("year_") + .order_by(sa.asc("year_")) # TODO add asc ordering ).fetchall() - yearsWithObs = DB.session.execute( - """ - SELECT distinct date_part('year', s.date_min) as year - FROM gn_synthese.synthese s - ORDER BY year DESC - """ + yearsWithObs = db.session.execute( + sa.select( + sa.func.distinct( + sa.cast(sa.func.date_part("year", VSynthese.date_min), sa.Integer) + ).label("year") + ).order_by(sa.desc(text("year"))) ).fetchall() - observations_by_group = DB.session.execute( - """ - SELECT count(*), t.group2_inpn - FROM gn_synthese.synthese s - JOIN taxonomie.taxref t ON t.cd_nom = s.cd_nom - WHERE date_part('year', s.date_min) = :year - GROUP BY t.group2_inpn - """, - {"year": year}, + + observations_by_group = db.session.execute( + sa.select( + sa.func.count(), + Taxref.group2_inpn, + ) + .select_from(VSynthese) + .join(Taxref, Taxref.cd_nom == VSynthese.cd_nom) + .where(sa.func.date_part("year", VSynthese.date_min) == year) + .group_by(Taxref.group2_inpn) ) t = { "yearsWithObs": [dict(row) for row in yearsWithObs], @@ -385,5 +536,5 @@ def refresh_vm(): """ Rafraîchissement des VM du dashboard """ - DB.session.execute(func.gn_dashboard.refresh_materialized_view_data()) - DB.session.commit() + db.session.execute(func.gn_dashboard.refresh_materialized_view_data()) + db.session.commit()