diff --git a/backend/geonature/core/gn_synthese/routes.py b/backend/geonature/core/gn_synthese/routes.py index c42c65711f..a2d00d6dd4 100644 --- a/backend/geonature/core/gn_synthese/routes.py +++ b/backend/geonature/core/gn_synthese/routes.py @@ -52,6 +52,7 @@ SyntheseLogEntry, ) from geonature.core.gn_synthese.synthese_config import MANDATORY_COLUMNS +from geonature.core.gn_synthese.utils.species_sheet import SpeciesSheetUtils from geonature.core.gn_synthese.utils.blurring import ( build_allowed_geom_cte, @@ -66,7 +67,6 @@ from geonature.core.gn_permissions.decorators import login_required, permissions_required from geonature.core.gn_permissions.tools import get_scopes_by_action, get_permissions from geonature.core.sensitivity.models import cor_sensitivity_area_type - from ref_geo.models import LAreas, BibAreasTypes from apptax.taxonomie.models import ( @@ -81,6 +81,8 @@ VMTaxrefListForautocomplete, ) +from geonature import app + routes = Blueprint("gn_synthese", __name__) @@ -965,28 +967,11 @@ def species_stats(scope, cd_ref): area_type = request.args.get("area_type") - if not area_type: + if not SpeciesSheetUtils.is_valid_area_type(area_type): raise BadRequest("Missing area_type parameter") - # Ensure area_type is valid - valid_area_types = ( - db.session.query(BibAreasTypes.type_code) - .distinct() - .filter(BibAreasTypes.type_code == area_type) - .scalar() - ) - if not valid_area_types: - raise BadRequest("Invalid area_type") - - # Subquery to fetch areas based on area_type - areas_subquery = ( - select([LAreas.id_area]) - .where(LAreas.id_type == BibAreasTypes.id_type) - .where(BibAreasTypes.type_code == area_type) - .alias("areas") - ) - - taxref_cd_nom_list = db.session.scalars(select(Taxref.cd_nom).where(Taxref.cd_ref == cd_ref)) + areas_subquery = SpeciesSheetUtils.get_area_subquery(area_type) + taxref_cd_nom_list = SpeciesSheetUtils.get_cd_nom_list_from_cd_ref(cd_ref) # Main query to fetch stats query = ( @@ -1014,9 +999,8 @@ def species_stats(scope, cd_ref): .where(Synthese.cd_nom.in_(taxref_cd_nom_list)) ) - synthese_query_obj = SyntheseQuery(Synthese, query, {}) - synthese_query_obj.filter_query_with_cruved(g.current_user, scope) - result = DB.session.execute(synthese_query_obj.query) + synthese_query = SpeciesSheetUtils.get_synthese_query_with_scope(g.current_user, scope, query) + result = DB.session.execute(synthese_query) synthese_stats = result.fetchone() data = { @@ -1033,6 +1017,40 @@ def species_stats(scope, cd_ref): return data +if app.config["SYNTHESE"]["SPECIES_SHEET"]["OBSERVERS"]["ENABLED"]: + + @routes.route("/species_observers/", methods=["GET"]) + @permissions.check_cruved_scope("R", get_scope=True, module_code="SYNTHESE") + # @json_resp + def species_observers(scope, cd_ref): + per_page = int(request.args.get("per_page", 1)) + page = request.args.get("page", 1, int) + + # taxref_cd_nom_list = SpeciesSheetUtils.get_cd_nom_list_from_cd_ref(cd_ref) + query = ( + db.session.query( + func.trim(func.unnest(func.string_to_array(Synthese.observers, ","))).label( + "observer" + ), + func.min(Synthese.date_min).label("date_min"), + func.max(Synthese.date_max).label("date_max"), + func.count(Synthese.observers).label("count"), + ).group_by("observer") + # .where(Synthese.cd_nom.in_(taxref_cd_nom_list)) + ) + query = SpeciesSheetUtils.get_synthese_query_with_scope(g.current_user, scope, query) + + results = query.paginate(page=page, per_page=per_page, error_out=False) + return jsonify( + { + "items": results.items, + "total": results.total, + "per_page": per_page, + "page": page, + } + ) + + @routes.route("/taxons_tree", methods=["GET"]) @login_required @json_resp diff --git a/backend/geonature/core/gn_synthese/synthese_config.py b/backend/geonature/core/gn_synthese/synthese_config.py index af5dc42935..98a8ea4a6f 100644 --- a/backend/geonature/core/gn_synthese/synthese_config.py +++ b/backend/geonature/core/gn_synthese/synthese_config.py @@ -138,6 +138,11 @@ class DefaultGeographicOverview: pass +class DefaultObservers: + ENABLED = True + pass + + class DefaultSpeciesSheet: ## DEFAULT SPECIES SHEET INDICATORS LIST_INDICATORS = [ diff --git a/backend/geonature/core/gn_synthese/utils/species_sheet.py b/backend/geonature/core/gn_synthese/utils/species_sheet.py new file mode 100644 index 0000000000..1b8f80f02b --- /dev/null +++ b/backend/geonature/core/gn_synthese/utils/species_sheet.py @@ -0,0 +1,44 @@ +import typing +from geonature.utils.env import db +from ref_geo.models import LAreas, BibAreasTypes + +from geonature.core.gn_synthese.models import Synthese +from sqlalchemy import select +from apptax.taxonomie.models import Taxref +from geonature.core.gn_synthese.utils.query_select_sqla import SyntheseQuery + + +class SpeciesSheetUtils: + + @staticmethod + def get_cd_nom_list_from_cd_ref(cd_ref: int) -> typing.List[int]: + return db.session.scalars(select(Taxref.cd_nom).where(Taxref.cd_ref == cd_ref)) + + @staticmethod + def get_synthese_query_with_scope(current_user, scope: int, query) -> any: + synthese_query_obj = SyntheseQuery(Synthese, query, {}) + synthese_query_obj.filter_query_with_cruved(current_user, scope) + return synthese_query_obj.query + + @staticmethod + def is_valid_area_type(area_type: str) -> bool: + # Ensure area_type is valid + valid_area_types = ( + db.session.query(BibAreasTypes.type_code) + .distinct() + .filter(BibAreasTypes.type_code == area_type) + .scalar() + ) + + return valid_area_types + + @staticmethod + def get_area_subquery(area_type: str) -> any: + + # Subquery to fetch areas based on area_type + return ( + select([LAreas.id_area]) + .where(LAreas.id_type == BibAreasTypes.id_type) + .where(BibAreasTypes.type_code == area_type) + .alias("areas") + ) diff --git a/backend/geonature/utils/config_schema.py b/backend/geonature/utils/config_schema.py index fd420ffb08..7e1fe31a0a 100644 --- a/backend/geonature/utils/config_schema.py +++ b/backend/geonature/utils/config_schema.py @@ -22,6 +22,7 @@ DEFAULT_EXPORT_COLUMNS, DEFAULT_LIST_COLUMN, DefaultGeographicOverview, + DefaultObservers, DefaultProfile, DefaultSpeciesSheet, ) @@ -282,6 +283,10 @@ class SpeciesSheetProfile(Schema): LIST_INDICATORS = fields.List(fields.Dict, load_default=DefaultProfile.LIST_INDICATORS) +class SpeciesSheetObservers(Schema): + ENABLED = fields.Boolean(load_default=DefaultObservers.ENABLED) + + class SpeciesSheetGeographicOverview(Schema): pass @@ -295,6 +300,7 @@ class SpeciesSheet(Schema): load_default=SpeciesSheetGeographicOverview().load({}) ) # rename PROFILE = fields.Nested(SpeciesSheetProfile, load_default=SpeciesSheetProfile().load({})) + OBSERVERS = fields.Dict(load_default=SpeciesSheetObservers().load({})) class Synthese(Schema): diff --git a/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data-pagination-item.ts b/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data-pagination-item.ts new file mode 100644 index 0000000000..07726a73ae --- /dev/null +++ b/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data-pagination-item.ts @@ -0,0 +1,11 @@ +export interface SyntheseDataPaginationItem { + totalItems: number; + currentPage: number; + perPage: number; +} + +export const DEFAULT_PAGINATION: SyntheseDataPaginationItem = { + totalItems: 0, + currentPage: 1, + perPage: 10, +}; diff --git a/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data.service.ts b/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data.service.ts index 85ec1862c1..aed6738d15 100644 --- a/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data.service.ts +++ b/frontend/src/app/GN2CommonModule/form/synthese-form/synthese-data.service.ts @@ -11,6 +11,7 @@ import { BehaviorSubject } from 'rxjs'; import { CommonService } from '@geonature_common/service/common.service'; import { Observable } from 'rxjs'; import { ConfigService } from '@geonature/services/config.service'; +import { DEFAULT_PAGINATION, SyntheseDataPaginationItem } from './synthese-data-pagination-item'; export const FormatMapMime = new Map([ ['csv', 'text/csv'], @@ -61,6 +62,18 @@ export class SyntheseDataService { }); } + getSyntheseSpeciesSheetObservers( + cd_ref: number, + pagination: SyntheseDataPaginationItem = DEFAULT_PAGINATION + ) { + return this._api.get(`${this.config.API_ENDPOINT}/synthese/species_observers/${cd_ref}`, { + params: { + per_page: pagination.perPage, + page: pagination.currentPage, + }, + }); + } + getTaxaCount(params = {}) { let queryString = new HttpParams(); for (let key in params) { diff --git a/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.html b/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.html new file mode 100644 index 0000000000..a2d9120b0c --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.html @@ -0,0 +1,15 @@ +
+ +
diff --git a/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.scss b/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.scss new file mode 100644 index 0000000000..2e38c350ec --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.scss @@ -0,0 +1,5 @@ +.Observers { + &__table { + box-shadow: none; + } +} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.ts b/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.ts new file mode 100644 index 0000000000..b75e24e1ff --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/tab-observers/tab-observers.component.ts @@ -0,0 +1,65 @@ +import { Component, OnInit } from '@angular/core'; +import { GN2CommonModule } from '@geonature_common/GN2Common.module'; +import { CommonModule } from '@angular/common'; +import { ConfigService } from '@geonature/services/config.service'; +import { Taxon } from '@geonature_common/form/taxonomy/taxonomy.component'; +import { TaxonSheetService } from '../taxon-sheet.service'; +import { SyntheseDataService } from '@geonature_common/form/synthese-form/synthese-data.service'; +import { + DEFAULT_PAGINATION, + SyntheseDataPaginationItem, +} from '@geonature_common/form/synthese-form/synthese-data-pagination-item'; +@Component({ + standalone: true, + selector: 'tab-observers', + templateUrl: 'tab-observers.component.html', + styleUrls: ['tab-observers.component.scss'], + imports: [GN2CommonModule, CommonModule], +}) +export class TabObserversComponent implements OnInit { + items: any[] = []; + pagination: SyntheseDataPaginationItem = DEFAULT_PAGINATION; + + readonly columns = [ + { prop: 'observer', name: 'Observateur' }, + { prop: 'date_min', name: 'Plus ancienne' }, + { prop: 'date_max', name: 'Plus récente' }, + { prop: 'count', name: "Nombre d'observations" }, + ]; + + constructor( + private _syntheseDataService: SyntheseDataService, + private _config: ConfigService, + private _tss: TaxonSheetService + ) {} + + ngOnInit() { + this._tss.taxon.subscribe((taxon: Taxon | null) => { + this.fetchObservers(); + }); + } + + onChangePage(event) { + this.pagination.currentPage = event.offset + 1; + this.fetchObservers(); + } + + fetchObservers() { + const taxon = this._tss.taxon.getValue(); + if (!taxon) { + console.log('taxon is undefined'); + return; + } + this._syntheseDataService + .getSyntheseSpeciesSheetObservers(taxon.cd_ref, this.pagination) + .subscribe((data) => { + // Store result + this.items = data.items; + this.pagination = { + totalItems: data.total, + currentPage: data.page, + perPage: data.per_page, + }; + }); + } +} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts index 5f73b07b46..a78388551d 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts +++ b/frontend/src/app/syntheseModule/taxon-sheet/taxon-sheet.route.service.ts @@ -11,6 +11,7 @@ import { ConfigService } from '@geonature/services/config.service'; import { Observable } from 'rxjs'; import { TabGeographicOverviewComponent } from './tab-geographic-overview/tab-geographic-overview.component'; import { TabProfileComponent } from './tab-profile/tab-profile.component'; +import { TabObserversComponent } from './tab-observers/tab-observers.component'; interface Tab { label: string; @@ -35,9 +36,17 @@ const ROUTE_PROFILE: Tab = { component: TabProfileComponent, }; +const ROUTE_OBSERVERS: Tab = { + label: 'Observateurs', + path: 'observers', + configEntry: 'OBSERVERS', + component: TabObserversComponent, +}; + export const ALL_TAXON_SHEET_ADVANCED_INFOS_ROUTES: Array = [ ROUTE_GEOGRAPHIC_OVERVIEW, ROUTE_PROFILE, + ROUTE_OBSERVERS, ]; @Injectable({ @@ -53,9 +62,13 @@ export class RouteService implements CanActivateChild { this.TAB_LINKS.push(ROUTE_MANDATORY); if (this._config && this._config['SYNTHESE'] && this._config['SYNTHESE']['SPECIES_SHEET']) { const config = this._config['SYNTHESE']['SPECIES_SHEET']; + console.log(config); if (config['PROFILE'] && config['PROFILE']['ENABLED']) { this.TAB_LINKS.push(ROUTE_PROFILE); } + if (config['OBSERVERS'] && config['OBSERVERS']['ENABLED']) { + this.TAB_LINKS.push(ROUTE_OBSERVERS); + } } }