Skip to content

Commit

Permalink
feat: extend species page
Browse files Browse the repository at this point in the history
  • Loading branch information
edelclaux authored and Pierre-Narcisi committed Sep 13, 2024
1 parent 23776ed commit 1caf4d0
Show file tree
Hide file tree
Showing 26 changed files with 831 additions and 153 deletions.
76 changes: 76 additions & 0 deletions backend/geonature/core/gn_synthese/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,82 @@ def general_stats(permissions):
return data


@routes.route("/species_stats/<int:cd_ref>", methods=["GET"])
@permissions.check_cruved_scope("R", get_scope=True, module_code="SYNTHESE")
@json_resp
def species_stats(scope, cd_ref):
"""Return stats about distinct species."""

area_type = request.args.get("area_type")

if not 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))

# Main query to fetch stats
query = (
select(
[
func.count(distinct(Synthese.id_synthese)).label("observation_count"),
func.count(distinct(Synthese.observers)).label("observer_count"),
func.count(distinct(areas_subquery.c.id_area)).label("area_count"),
func.min(Synthese.altitude_min).label("altitude_min"),
func.max(Synthese.altitude_max).label("altitude_max"),
func.min(Synthese.date_min).label("date_min"),
func.max(Synthese.date_max).label("date_max"),
]
)
.select_from(
sa.join(
Synthese,
CorAreaSynthese,
Synthese.id_synthese == CorAreaSynthese.id_synthese,
)
.join(areas_subquery, CorAreaSynthese.id_area == areas_subquery.c.id_area)
.join(LAreas, CorAreaSynthese.id_area == LAreas.id_area)
.join(BibAreasTypes, LAreas.id_type == BibAreasTypes.id_type)
)
.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_stats = result.fetchone()

data = {
"cd_ref": cd_ref,
"observation_count": synthese_stats["observation_count"],
"observer_count": synthese_stats["observer_count"],
"area_count": synthese_stats["area_count"],
"altitude_min": synthese_stats["altitude_min"],
"altitude_max": synthese_stats["altitude_max"],
"date_min": synthese_stats["date_min"],
"date_max": synthese_stats["date_max"],
}

return data


@routes.route("/taxons_tree", methods=["GET"])
@login_required
@json_resp
Expand Down
74 changes: 74 additions & 0 deletions backend/geonature/core/gn_synthese/synthese_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,77 @@
{"prop": "dataset_name", "name": "JDD", "max_width": 200},
{"prop": "observers", "name": "observateur", "max_width": 200},
]


class DefaultProfile:
ENABLED = True
## DEFAULT PROFILE INDICATORS
LIST_INDICATORS = [
{
"name": "observation(s) valide(s)",
"matIcon": "search",
"field": "count_valid_data",
"type": "number",
},
{
"name": "Première observation",
"matIcon": "schedule",
"field": "first_valid_data",
"type": "date",
},
{
"name": "Dernière observation",
"matIcon": "search",
"field": "last_valid_data",
"type": "date",
},
{
"name": "Plage d'altitude(s)",
"matIcon": "terrain",
"field": ["altitude_min", "altitude_max"],
"unit": "m",
"type": "number",
},
]


class DefaultGeographicOverview:
pass


class DefaultSpeciesSheet:
## DEFAULT SPECIES SHEET INDICATORS
LIST_INDICATORS = [
{
"name": "observation(s)",
"matIcon": "search",
"field": "observation_count",
"type": "number",
},
{
"name": "observateur(s)",
"matIcon": "people",
"field": "observer_count",
"type": "number",
},
{
"name": "commune(s)",
"matIcon": "location_on",
"field": "area_count",
"type": "number",
},
{
"name": "Plage d'altitude(s)",
"matIcon": "terrain",
"unit": "m",
"type": "number",
"field": ["altitude_min", "altitude_max"],
},
{
"name": "Plage d'observation(s)",
"matIcon": "date_range",
"type": "date",
"field": ["date_min", "date_max"],
"separator": "-",
},
]
27 changes: 27 additions & 0 deletions backend/geonature/utils/config_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
from geonature.core.gn_synthese.synthese_config import (
DEFAULT_EXPORT_COLUMNS,
DEFAULT_LIST_COLUMN,
DefaultGeographicOverview,
DefaultProfile,
DefaultSpeciesSheet,
)
from geonature.utils.env import GEONATURE_VERSION, BACKEND_DIR, ROOT_DIR
from geonature.utils.module import iter_modules_dist, get_module_config
Expand Down Expand Up @@ -274,6 +277,26 @@ class ExportObservationSchema(Schema):
geojson_local_field = fields.String(load_default="geojson_local")


class SpeciesSheetProfile(Schema):
ENABLED = fields.Boolean(load_default=DefaultProfile.ENABLED)
LIST_INDICATORS = fields.List(fields.Dict, load_default=DefaultProfile.LIST_INDICATORS)


class SpeciesSheetGeographicOverview(Schema):
pass


class SpeciesSheet(Schema):
# --------------------------------------------------------------------
# SYNTHESE - SPECIES_SHEET
LIST_INDICATORS = fields.List(fields.Dict, load_default=DefaultSpeciesSheet.LIST_INDICATORS)

GEOGRAPHIC_OVERVIEW = fields.Dict(
load_default=SpeciesSheetGeographicOverview().load({})
) # rename
PROFILE = fields.Nested(SpeciesSheetProfile, load_default=SpeciesSheetProfile().load({}))


class Synthese(Schema):
# --------------------------------------------------------------------
# SYNTHESE - SEARCH FORM
Expand Down Expand Up @@ -428,6 +451,10 @@ class Synthese(Schema):
# Activate the blurring of sensitive observations. Otherwise, exclude them
BLUR_SENSITIVE_OBSERVATIONS = fields.Boolean(load_default=True)

# --------------------------------------------------------------------
# SYNTHESE - SPECIES_SHEET
SPECIES_SHEET = fields.Nested(SpeciesSheet, load_default=SpeciesSheet().load({}))

@pre_load
def warn_deprecated(self, data, **kwargs):
deprecated = {
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/app/GN2CommonModule/form/data-form.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface ParamsDict {
[key: string]: any;
}

export type Profile = GeoJSON.Feature;

export const FormatMapMime = new Map([
['csv', 'text/csv'],
['json', 'application/json'],
Expand Down Expand Up @@ -642,8 +644,10 @@ export class DataFormService {
return this._http.get<any>(`${this.config.API_TAXHUB}/bdc_statuts/status_values/${statusType}`);
}

getProfile(cdRef) {
return this._http.get<any>(`${this.config.API_ENDPOINT}/gn_profiles/valid_profile/${cdRef}`);
getProfile(cdRef): Observable<Profile> {
return this._http.get<Profile>(
`${this.config.API_ENDPOINT}/gn_profiles/valid_profile/${cdRef}`
);
}

getPhenology(cdRef, idNomenclatureLifeStage?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ export class SyntheseDataService {
return this._api.get<any>(`${this.config.API_ENDPOINT}/synthese/general_stats`);
}

getSyntheseSpeciesSheetStat(cd_ref: number, areaType: string = 'COM') {
return this._api.get<any>(`${this.config.API_ENDPOINT}/synthese/species_stats/${cd_ref}`, {
params: new HttpParams().append('area_type', areaType),
});
}

getTaxaCount(params = {}) {
let queryString = new HttpParams();
for (let key in params) {
Expand Down
35 changes: 32 additions & 3 deletions frontend/src/app/syntheseModule/synthese.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,33 @@ import { SyntheseModalDownloadComponent } from './synthese-results/synthese-list
import { DiscussionCardComponent } from '@geonature/shared/discussionCardModule/discussion-card.component';
import { AlertInfoComponent } from '../shared/alertInfoModule/alert-Info.component';
import { TaxonSheetComponent } from './taxon-sheet/taxon-sheet.component';
import {
RouteService,
ALL_TAXON_SHEET_ADVANCED_INFOS_ROUTES,
ROUTE_MANDATORY,
} from './taxon-sheet/taxon-sheet.route.service';

const routes: Routes = [
{ path: '', component: SyntheseComponent },
{ path: 'occurrence/:id_synthese', component: SyntheseComponent, pathMatch: 'full' },
{ path: 'taxon/:cd_nom', component: TaxonSheetComponent },
{
path: 'taxon/:cd_ref',
component: TaxonSheetComponent,
canActivateChild: [RouteService],
children: [
{
path: '',
redirectTo: ROUTE_MANDATORY.path,
pathMatch: 'prefix',
},
...ALL_TAXON_SHEET_ADVANCED_INFOS_ROUTES.map((tab) => {
return {
path: tab.path,
component: tab.component,
};
}),
],
},
];

@NgModule({
Expand All @@ -29,20 +52,26 @@ const routes: Routes = [
SharedSyntheseModule,
CommonModule,
TreeModule,
TaxonSheetComponent,
],
declarations: [
SyntheseComponent,
SyntheseListComponent,
SyntheseCarteComponent,
SyntheseModalDownloadComponent,
TaxonSheetComponent,
],
entryComponents: [
SyntheseInfoObsComponent,
SyntheseModalDownloadComponent,
DiscussionCardComponent,
AlertInfoComponent,
],
providers: [MapService, DynamicFormService, TaxonAdvancedStoreService, SyntheseFormService],
providers: [
MapService,
DynamicFormService,
TaxonAdvancedStoreService,
SyntheseFormService,
RouteService,
],
})
export class SyntheseModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div [className]="small ? 'card Indicator Indicator--small' : 'card Indicator'">
<mat-icon class="Indicator__icon">{{ indicator.matIcon }}</mat-icon>
<span class="Indicator__value">{{ indicator.value }}</span>
<span class="Indicator__name text-secondary">{{ indicator.name }}</span>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.Indicator {
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
row-gap: 0rem;
padding: 1rem;
height: 9rem;
width: auto;
min-width: 12rem;
&__icon {
$icon-size: 2rem;
font-size: $icon-size;
height: $icon-size;
width: $icon-size;
min-height: $icon-size;
}
&__value {
font-size: 1.5rem;
white-space: nowrap;
&Unit {
opacity: 0.5;
}
}
&__name {
font-size: 1rem;
text-align: center;
}
}

.Indicator--small {
border: none;
height: auto;
.Indicator__icon {
$icon-size: 1.5rem;
font-size: $icon-size;
height: $icon-size;
width: $icon-size;
min-height: $icon-size;
}
.Indicator__value {
font-size: 1.3rem;
}
.Indicator__name {
font-size: 1rem;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Component, Input } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { Indicator } from './indicator';

@Component({
standalone: true,
selector: 'indicator',
templateUrl: 'indicator.component.html',
styleUrls: ['indicator.component.scss'],
imports: [MatIconModule],
})
export class IndicatorComponent {
@Input()
indicator: Indicator;

@Input()
small: boolean = false;
}
Loading

0 comments on commit 1caf4d0

Please sign in to comment.