Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fiche taxon] Ajout d'un onglet "Observateurs" dans la fiche taxon #3204

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
86 changes: 63 additions & 23 deletions backend/geonature/core/gn_synthese/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from werkzeug.exceptions import Forbidden, NotFound, BadRequest, Conflict
from werkzeug.datastructures import MultiDict
from sqlalchemy import distinct, func, desc, asc, select, case
from sqlalchemy.orm import joinedload, lazyload, selectinload, contains_eager
from sqlalchemy.orm import joinedload, lazyload, selectinload, contains_eager, Query
from geojson import FeatureCollection, Feature
import sqlalchemy as sa
from sqlalchemy.orm import load_only, aliased, Load, with_expression
Expand All @@ -43,6 +43,7 @@
from geonature.core.gn_synthese.models import (
BibReportsTypes,
CorAreaSynthese,
CorObserverSynthese,
DefaultsNomenclaturesValue,
Synthese,
TSources,
Expand All @@ -51,7 +52,12 @@
TReport,
SyntheseLogEntry,
)
from geonature.core.gn_commons.models import TMedias

from pypnusershub.db import User

from geonature.core.gn_synthese.synthese_config import MANDATORY_COLUMNS
from geonature.core.gn_synthese.utils.taxon_sheet import TaxonSheetUtils, SortOrder

from geonature.core.gn_synthese.utils.blurring import (
build_allowed_geom_cte,
Expand All @@ -66,7 +72,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 (
Expand All @@ -81,6 +86,8 @@
VMTaxrefListForautocomplete,
)

from geonature import app


routes = Blueprint("gn_synthese", __name__)

Expand Down Expand Up @@ -968,25 +975,11 @@ def taxon_stats(scope, cd_ref):
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")
)
if not TaxonSheetUtils.is_valid_area_type(area_type):
raise BadRequest("Invalid area_type parameter")

taxref_cd_nom_list = db.session.scalars(select(Taxref.cd_nom).where(Taxref.cd_ref == cd_ref))
areas_subquery = TaxonSheetUtils.get_area_subquery(area_type)
taxref_cd_nom_list = TaxonSheetUtils.get_cd_nom_list_from_cd_ref(cd_ref)

# Main query to fetch stats
query = (
Expand Down Expand Up @@ -1014,9 +1007,8 @@ def taxon_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 = TaxonSheetUtils.get_synthese_query_with_scope(g.current_user, scope, query)
result = DB.session.execute(synthese_query)
synthese_stats = result.fetchone()

data = {
Expand All @@ -1033,6 +1025,54 @@ def taxon_stats(scope, cd_ref):
return data


if app.config["SYNTHESE"]["TAXON_SHEET"]["ENABLE_OBSERVERS"]:

@routes.route("/taxon_observers/<int:cd_ref>", methods=["GET"])
@permissions.check_cruved_scope("R", get_scope=True, module_code="SYNTHESE")
# @json_resp
def taxon_observers(scope, cd_ref):
per_page = request.args.get("per_page", 10, int)
page = request.args.get("page", 1, int)
sort_by = request.args.get("sort_by", "observer")
sort_order = request.args.get("sort_order", SortOrder.ASC, SortOrder)
field_separator = request.args.get(
"field_separator", app.config["SYNTHESE"]["FIELD_OBSERVERS_SEPARATOR"]
)

# Handle sorting
if sort_by not in ["observer", "date_min", "date_max", "observation_count", "media_count"]:
raise BadRequest(f"The sort_by column {sort_by} is not defined")

taxref_cd_nom_list = TaxonSheetUtils.get_cd_nom_list_from_cd_ref(cd_ref)

query = (
db.session.query(
func.trim(
func.unnest(func.string_to_array(Synthese.observers, field_separator))
).label("observer"),
func.min(Synthese.date_min).label("date_min"),
func.max(Synthese.date_max).label("date_max"),
func.count(Synthese.id_synthese).label("observation_count"),
func.count(TMedias.id_media).label("media_count"),
)
.group_by("observer")
.outerjoin(Synthese.medias)
.where(Synthese.cd_nom.in_(taxref_cd_nom_list))
)
query = TaxonSheetUtils.get_synthese_query_with_scope(g.current_user, scope, query)
query = TaxonSheetUtils.update_query_with_sorting(query, sort_by, sort_order)
results = TaxonSheetUtils.paginate(query, page, per_page)

return jsonify(
{
"items": results.items,
"total": results.total,
"per_page": per_page,
"page": page,
}
)


@routes.route("/taxons_tree", methods=["GET"])
@login_required
@json_resp
Expand Down
65 changes: 65 additions & 0 deletions backend/geonature/core/gn_synthese/utils/taxon_sheet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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, desc, asc
from apptax.taxonomie.models import Taxref
from geonature.core.gn_synthese.utils.query_select_sqla import SyntheseQuery
from sqlalchemy.orm import Query
from werkzeug.exceptions import BadRequest
from flask_sqlalchemy.pagination import Pagination
from enum import Enum


class SortOrder(Enum):
ASC = "asc"
DESC = "desc"


class TaxonSheetUtils:

@staticmethod
def update_query_with_sorting(query: Query, sort_by: str, sort_order: SortOrder) -> Query:
if sort_order == SortOrder.ASC:
return query.order_by(asc(sort_by))

return query.order_by(desc(sort_by))

@staticmethod
def paginate(query: Query, page: int, per_page: int) -> Pagination:
return query.paginate(page=page, per_page=per_page, error_out=False)

#
@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: Query) -> SyntheseQuery:
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) -> Query:

# 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")
)
13 changes: 8 additions & 5 deletions backend/geonature/tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,12 +328,12 @@ def create_user(
db.session.add(organisme)

users_to_create = [
(("noright_user", organisme, 0), {}),
(("stranger_user", None, 2), {}),
(("associate_user", organisme, 2), {}),
(("self_user", organisme, 1), {}),
(("noright_user", organisme, 0), {"nom_role": "User", "prenom_role": "NoRight"}),
(("stranger_user", None, 2), {"nom_role": "User", "prenom_role": "Stranger"}),
(("associate_user", organisme, 2), {"nom_role": "User", "prenom_role": "Associate"}),
(("self_user", organisme, 1), {"nom_role": "User", "prenom_role": "Self"}),
(("user", organisme, 2), {"nom_role": "Bob", "prenom_role": "Bobby"}),
(("admin_user", organisme, 3), {}),
(("admin_user", organisme, 3), {"nom_role": "Administrateur", "prenom_role": "Test"}),
(("associate_user_2_exclude_sensitive", organisme, 2, True), {}),
(
(
Expand Down Expand Up @@ -523,6 +523,7 @@ def create_synthese(
source,
uuid=func.uuid_generate_v4(),
cor_observers=[],
observers=[],
date_min="",
date_max="",
altitude_min=800,
Expand Down Expand Up @@ -551,6 +552,7 @@ def create_synthese(
altitude_min=altitude_min,
altitude_max=altitude_max,
cor_observers=cor_observers,
observers=observers,
**kwargs,
)

Expand Down Expand Up @@ -708,6 +710,7 @@ def synthese_data(app, users, datasets, source, sources_modules):
source_m,
unique_id_sinp,
[users["admin_user"], users["user"]],
["Administrative Test", "Bobby Bob"],
date_min,
date_max,
altitude_min,
Expand Down
122 changes: 121 additions & 1 deletion backend/geonature/tests/test_synthese.py
Original file line number Diff line number Diff line change
Expand Up @@ -1154,7 +1154,7 @@ def test_taxon_stats(self, synthese_data, users):
url_for("gn_synthese.taxon_stats", cd_ref=CD_REF_VALID, area_type=AREA_TYPE_INVALID),
)
assert response.status_code == 400
assert response.json["description"] == "Invalid area_type"
assert response.json["description"] == "Invalid area_type parameter"

# Invalid cd_ref parameter
response = self.client.get(
Expand Down Expand Up @@ -1225,6 +1225,126 @@ def test_get_one_synthese_record(self, app, users, synthese_data):
)
assert response.status_code == Forbidden.code

def test_taxon_observer(self, synthese_data, users):
set_logged_user(self.client, users["stranger_user"])

## Test Data

SORT_ORDER_UNDEFINED = "sort-order-undefined"
SORT_ORDER_ASC = "asc"
SORT_ORDER_DESC = "desc"
PER_PAGE = 2
SORT_BY_UNDEFINED = "sort-by-undefined"

CD_REF = 2497
CD_REF_OBSERVERS_ASC = {
"items": [
{
"date_max": "Thu, 03 Oct 2024 08:09:10 GMT",
"date_min": "Wed, 02 Oct 2024 11:22:33 GMT",
"media_count": 0,
"observation_count": 3,
"observer": "Administrateur Test",
},
{
"date_max": "Thu, 03 Oct 2024 08:09:10 GMT",
"date_min": "Wed, 02 Oct 2024 11:22:33 GMT",
"media_count": 0,
"observation_count": 3,
"observer": "Bob Bobby",
},
],
"page": 1,
"per_page": 2,
"total": 2,
}
CD_REF_OBSERVERS_DESC = {
"items": [
{
"date_max": "Thu, 03 Oct 2024 08:09:10 GMT",
"date_min": "Wed, 02 Oct 2024 11:22:33 GMT",
"media_count": 0,
"observation_count": 3,
"observer": "Bob Bobby",
},
{
"date_max": "Thu, 03 Oct 2024 08:09:10 GMT",
"date_min": "Wed, 02 Oct 2024 11:22:33 GMT",
"media_count": 0,
"observation_count": 3,
"observer": "Administrateur Test",
},
],
"page": 1,
"per_page": 2,
"total": 2,
}

## sort_order

# Unknow sort_order parameters: shoudl fallback in asc
response = self.client.get(
url_for(
"gn_synthese.taxon_observers",
cd_ref=CD_REF,
per_page=PER_PAGE,
sort_order=SORT_ORDER_UNDEFINED,
),
)
assert response.status_code == 200
assert response.get_json() == CD_REF_OBSERVERS_ASC

# sort order ASC
response = self.client.get(
url_for(
"gn_synthese.taxon_observers",
cd_ref=CD_REF,
per_page=PER_PAGE,
sort_order=SORT_ORDER_ASC,
),
)
assert response.status_code == 200
assert response.get_json() == CD_REF_OBSERVERS_ASC

# sort order DESC
response = self.client.get(
url_for(
"gn_synthese.taxon_observers",
cd_ref=CD_REF,
per_page=PER_PAGE,
sort_order=SORT_ORDER_DESC,
),
)
assert response.status_code == 200
assert response.get_json() == CD_REF_OBSERVERS_DESC

## sort_by
response = self.client.get(
url_for(
"gn_synthese.taxon_observers",
cd_ref=CD_REF,
per_page=PER_PAGE,
sort_order=SORT_ORDER_ASC,
sort_by=SORT_BY_UNDEFINED,
),
)
assert response.status_code == BadRequest.code
assert (
response.json["description"] == f"The sort_by column {SORT_BY_UNDEFINED} is not defined"
)

# Ok
response = self.client.get(
url_for(
"gn_synthese.taxon_observers",
cd_ref=CD_REF,
per_page=PER_PAGE,
)
)

assert response.status_code == 200
assert response.get_json() == CD_REF_OBSERVERS_ASC

def test_color_taxon(self, synthese_data, users):
# Note: require grids 5×5!
set_logged_user(self.client, users["self_user"])
Expand Down
Loading
Loading