Skip to content

Commit e110c63

Browse files
committed
Merge remote-tracking branch upstream/dev into origin/dev
2 parents c9058a3 + 52f3c03 commit e110c63

33 files changed

Lines changed: 330 additions & 147 deletions

.pre-commit-config.yaml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,19 @@ repos:
3939
"--config=setup.cfg",
4040
"--select=E9,F63,F7,F82,QGS101,QGS102,QGS103,QGS104,QGS106",
4141
]
42+
- repo: local
43+
hooks:
44+
- id: generate-requirements
45+
name: Generate Python Requirements
46+
entry: |
47+
bash -c "cd backend && \
48+
poetry export -f requirements.txt -o requirements.txt --without-hashes && \
49+
poetry export -f requirements.txt --only=dev -o requirements-dev.txt --without-hashes && \
50+
poetry export -f requirements.txt --only=docs -o requirements-docs.txt --without-hashes"
51+
language: system
52+
types: [python]
4253

4354
ci:
44-
autoupdate_schedule: quarterly
45-
skip: []
46-
submodules: false
55+
autoupdate_schedule: quarterly
56+
skip: []
57+
submodules: false

CHANGELOG.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,29 @@
11
# CHANGELOG
22

3-
## 1.3.0 - Unreleased
3+
## 1.3.2 - 2025-12-01
4+
5+
> [!WARNING]
6+
> **Version 1.3.x will be the latest release compatible with python 3.9**
7+
8+
### :bug: Fixes
9+
10+
* Refix servor error when creating or updating programs (missing requirements up to date, temporary patch to fix #477).
11+
12+
### :technologist: Development
13+
14+
* New Makefile to facilitate certain maintenance operations
15+
* Requirements are now generated automatically during commit validation using pre-commit.
16+
17+
## 1.3.1 - 2025-11-30
18+
19+
> [!WARNING]
20+
> **Version 1.3.x will be the latest release compatible with python 3.9**
21+
22+
### :bug: Fixes
23+
24+
* Fix servor error when creating or updating programs (temporary patch to fix #477).
25+
26+
## 1.3.0 - 2025-11-26
427

528
> [!WARNING]
629
> **Require TaxHub 2.x (i.e. GeoNature >= 2.15.x)**
@@ -22,7 +45,7 @@
2245
### :technologist: Development
2346

2447
* Add calls to two TaxHub API routes in order to get media types and bibattribut.
25-
* Add component taxonomy-research which calls `allnamebylist` TaxHub API route in Form Observation Program
48+
* Add component taxonomy-research which calls `allnamebylist` TaxHub API route in Form Observation Program
2649
* Remove TaxHub list from cache backend
2750

2851
### :speech_balloon: Release note

Makefile

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Define variables
2+
APP_NAME := gncitizen
3+
BACKEND_DIR := backend
4+
FRONTEND_DIR := frontend
5+
DOCKER_COMPOSE_FILE := docker-compose.yml
6+
7+
include .env
8+
9+
DOCKER_IMAGE_BACKEND := $(APP_NAME)-backend
10+
DOCKER_IMAGE_FRONTEND := $(APP_NAME)-frontend
11+
12+
# Phony targets
13+
.PHONY: all install-dependencies generate-requirements build docker-start clean
14+
15+
# Default target
16+
all: install-dependencies generate-requirements build docker-start
17+
18+
# Install dependencies for frontend and backend
19+
install-dependencies:
20+
cd $(BACKEND_DIR) && \
21+
if [ -f poetry.lock ]; then \
22+
poetry install; \
23+
elif [ -f requirements.txt ]; then \
24+
pip install -r requirements.txt; \
25+
fi
26+
cd $(FRONTEND_DIR) && npm install
27+
28+
# Generate Python requirements files
29+
generate-requirements:
30+
cd $(BACKEND_DIR) && \
31+
poetry export -f requirements.txt -o requirements.txt --without-hashes && \
32+
poetry export -f requirements.txt --only=dev -o requirements-dev.txt --without-hashes && \
33+
poetry export -f requirements.txt --only=docs -o requirements-docs.txt --without-hashes
34+
35+
# Build Docker images for backend and frontend
36+
build:
37+
docker build -t $(DOCKER_IMAGE_BACKEND) -f $(BACKEND_DIR)/Dockerfile $(BACKEND_DIR)
38+
docker build -t $(DOCKER_IMAGE_FRONTEND) -f $(FRONTEND_DIR)/Dockerfile $(FRONTEND_DIR)
39+
40+
# Start project using docker-compose
41+
docker-start:
42+
docker-compose -f $(DOCKER_COMPOSE_FILE) up -d
43+
44+
# Clean up Docker containers and images
45+
clean:
46+
docker-compose -f $(DOCKER_COMPOSE_FILE) down
47+
docker rmi $(DOCKER_IMAGE_BACKEND) $(DOCKER_IMAGE_FRONTEND)

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ Documentation : https://geonature-citizen.readthedocs.io
3535
- https://observation.lehavre.fr
3636
- https://enquetes.cbiodiv.org
3737
- https://jobservemonparc.fr
38+
- https://obs.parc-livradois-forez.org
39+
- https://citizen.parc-naturel-pilat.fr
40+
- https://observatoire-biodiversite.parcdesbauges.com
3841

3942
## Solutions logicielles
4043

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.3.0
1+
1.3.2

backend/gncitizen/core/observations/models.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"comment",
2828
"timestamp_create",
2929
"json_data",
30-
"name"
30+
"name",
3131
)
3232

3333
TAXREF_KEYS = ["nom_vern", "cd_nom", "cd_ref", "lb_nom"]
@@ -155,9 +155,7 @@ def get_feature(self):
155155
"""get obs data as geojson feature"""
156156

157157
result_dict = self.as_dict(True)
158-
result_dict["observer"] = (
159-
self.observer.as_simple_dict() if self.observer else None
160-
)
158+
result_dict["observer"] = self.observer.as_simple_dict() if self.observer else None
161159
result_dict["validator"] = (
162160
self.validator_ref.as_simple_dict() if self.validator_ref else None
163161
)
@@ -171,9 +169,7 @@ def get_feature(self):
171169
for k in result_dict:
172170
if k in OBS_KEYS:
173171
feature["properties"][k] = (
174-
result_dict[k].name
175-
if isinstance(result_dict[k], Enum)
176-
else result_dict[k]
172+
result_dict[k].name if isinstance(result_dict[k], Enum) else result_dict[k]
177173
)
178174
feature["properties"]["photos"] = [
179175
{

backend/gncitizen/core/observations/routes.py

Lines changed: 18 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@
99
from flask_jwt_extended import jwt_required
1010
from geoalchemy2.shape import from_shape
1111
from geojson import FeatureCollection
12-
from shapely.geometry import Point, shape
13-
from sqlalchemy import desc
14-
from utils_flask_sqla.response import json_resp
15-
1612
from gncitizen.core.commons.models import MediaModel, ProgramsModel
1713
from gncitizen.utils.env import admin
1814
from gncitizen.utils.errors import GeonatureApiError
@@ -27,6 +23,9 @@
2723
taxhub_rest_get_taxon_list,
2824
)
2925
from server import db
26+
from shapely.geometry import Point, shape
27+
from sqlalchemy import desc
28+
from utils_flask_sqla.response import json_resp
3029

3130
from .admin import ObservationView
3231
from .models import (
@@ -247,9 +246,8 @@ def post_observation():
247246
newobs.id_observation,
248247
ObservationMediaModel,
249248
)
250-
current_app.logger.debug(
251-
"[post_observation] ObsTax UPLOAD FILE {}".format(file)
252-
)
249+
current_app.logger.debug("[post_observation] ObsTax UPLOAD FILE {}".format(file))
250+
253251
newobs = (
254252
db.session.query(ObservationModel)
255253
.options(db.joinedload(ObservationModel.medias))
@@ -265,13 +263,9 @@ def post_observation():
265263
else:
266264
taxon_list_data = None
267265

268-
features_with_taxhub_info = set_taxa_info_from_taxhub(
269-
taxon_list_data, [features]
270-
)
266+
features_with_taxhub_info = set_taxa_info_from_taxhub(taxon_list_data, [features])
271267
except Exception as e:
272-
current_app.logger.warning(
273-
"[post_observation] ObsTax ERROR ON FILE SAVING", str(e)
274-
)
268+
current_app.logger.warning("[post_observation] ObsTax ERROR ON FILE SAVING", str(e))
275269
# raise GeonatureApiError(e)
276270
return (
277271
{
@@ -324,7 +318,7 @@ def get_all_observations() -> Union[FeatureCollection, Tuple[Dict, int]]:
324318
ProgramsModel.id_program == ObservationModel.id_program,
325319
isouter=True,
326320
)
327-
.order_by(desc(ObservationModel.timestamp_create))
321+
.order_by(desc(ObservationModel.date))
328322
.filter(*filters)
329323
)
330324

@@ -347,9 +341,7 @@ def get_all_observations() -> Union[FeatureCollection, Tuple[Dict, int]]:
347341

348342
if id_taxonomy_list or cd_nom_list:
349343
taxon_list_data = taxhub_rest_get_taxon_list(params_to_update=params)
350-
features_with_taxhub_info = set_taxa_info_from_taxhub(
351-
taxon_list_data, features
352-
)
344+
features_with_taxhub_info = set_taxa_info_from_taxhub(taxon_list_data, features)
353345
else:
354346
features_with_taxhub_info = features
355347

@@ -408,10 +400,7 @@ def update_observation():
408400
observation_to_update = ObservationModel.query.filter_by(
409401
id_observation=request.form.get("id_observation")
410402
)
411-
if (
412-
observation_to_update.one().id_role != current_user.id_user
413-
and not current_user.validator
414-
):
403+
if observation_to_update.one().id_role != current_user.id_user and not current_user.validator:
415404
abort(403, "unauthorized")
416405

417406
try:
@@ -434,9 +423,7 @@ def update_observation():
434423
shape_geometry = shape(geometry)
435424
update_obs["geom"] = from_shape(shape_geometry, srid=4326)
436425
if not update_obs["municipality"]:
437-
update_obs["municipality"] = get_municipality_id_from_wkb(
438-
shape_geometry
439-
)
426+
update_obs["municipality"] = get_municipality_id_from_wkb(shape_geometry)
440427
except Exception as e:
441428
current_app.logger.warning("[post_observation] coords ", e)
442429
raise GeonatureApiError(e)
@@ -457,8 +444,7 @@ def update_observation():
457444
if len(id_media_to_delete):
458445
db.session.query(ObservationMediaModel).filter(
459446
ObservationMediaModel.id_media.in_(tuple(id_media_to_delete)),
460-
ObservationMediaModel.id_data_source
461-
== update_data.get("id_observation"),
447+
ObservationMediaModel.id_data_source == update_data.get("id_observation"),
462448
).delete(synchronize_session="fetch")
463449
db.session.query(MediaModel).filter(
464450
MediaModel.id_media.in_(tuple(id_media_to_delete))
@@ -475,14 +461,10 @@ def update_observation():
475461
update_data.get("id_observation"),
476462
ObservationMediaModel,
477463
)
478-
current_app.logger.debug(
479-
"[post_observation] ObsTax UPLOAD FILE {}".format(file)
480-
)
464+
current_app.logger.debug("[post_observation] ObsTax UPLOAD FILE {}".format(file))
481465

482466
except Exception as e:
483-
current_app.logger.warning(
484-
"[post_observation] ObsTax ERROR ON FILE SAVING", str(e)
485-
)
467+
current_app.logger.warning("[post_observation] ObsTax ERROR ON FILE SAVING", str(e))
486468
# raise GeonatureApiError(e)
487469
obs_validation = (
488470
"non_validatable_status" in update_data
@@ -499,9 +481,7 @@ def update_observation():
499481
new_validation_status = ValidationStatus.VALIDATED
500482
if non_validatable_status:
501483
status = [
502-
s
503-
for s in INVALIDATION_STATUSES
504-
if s["value"] == non_validatable_status
484+
s for s in INVALIDATION_STATUSES if s["value"] == non_validatable_status
505485
][0]
506486
new_validation_status = ValidationStatus[status["link"]]
507487

@@ -525,9 +505,7 @@ def update_observation():
525505
try:
526506
observer = obs_to_update_obj.observer
527507
send_user_email(
528-
subject=current_app.config["VALIDATION_EMAIL"][
529-
"SUBJECT"
530-
].format(
508+
subject=current_app.config["VALIDATION_EMAIL"]["SUBJECT"].format(
531509
program=f"{obs_to_update_obj.program_ref.title}",
532510
observation=f"{obs_to_update_obj.name} (#{obs_to_update_obj.id_observation})",
533511
),
@@ -541,12 +519,8 @@ def update_observation():
541519
),
542520
)
543521
except Exception as e:
544-
current_app.logger.warning(
545-
"send validation_email failed. %s", str(e)
546-
)
547-
return {
548-
"message": f"""send validation_email failed: "{str(e)}" """
549-
}, 400
522+
current_app.logger.warning("send validation_email failed. %s", str(e))
523+
return {"message": f"""send validation_email failed: "{str(e)}" """}, 400
550524

551525
return ("observation updated successfully"), 200
552526
except Exception as e:

backend/gncitizen/core/sites/routes.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,14 @@ def get_program_sites(id):
226226
description: List of all sites
227227
"""
228228
try:
229-
sites = SiteModel.query.filter_by(id_program=id).all()
229+
sites = (
230+
db.session.query(SiteModel)
231+
.join(VisitModel, SiteModel.id_site == VisitModel.id_site)
232+
.filter(SiteModel.id_program == id)
233+
.order_by(VisitModel.date.desc())
234+
.order_by(SiteModel.timestamp_create.desc())
235+
.all()
236+
)
230237
return prepare_sites(sites)
231238
except Exception as e:
232239
return {"error_message": str(e)}, 400

backend/gncitizen/core/taxonomy/routes.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
from typing import Any, Dict, List, Union
22

33
from flask import Blueprint, current_app, request
4-
from utils_flask_sqla.response import json_resp
5-
64
from gncitizen.utils.taxonomy import (
75
get_all_attributes,
86
get_all_medias_types,
@@ -12,6 +10,7 @@
1210
taxhub_rest_get_all_lists,
1311
taxhub_rest_get_taxon_list,
1412
)
13+
from utils_flask_sqla.response import json_resp
1514

1615
taxo_api = Blueprint("taxonomy", __name__)
1716

backend/gncitizen/utils/taxonomy.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,7 @@ def taxhub_rest_get_all_lists() -> Optional[Dict]:
7474
if res.status_code == 200:
7575
try:
7676
taxa_lists = res.json()["data"]
77-
taxa_lists = [
78-
taxa for taxa in taxa_lists if not taxa["id_liste"] in excluded_list_ids
79-
]
77+
taxa_lists = [taxa for taxa in taxa_lists if not taxa["id_liste"] in excluded_list_ids]
8078
for taxa_list in taxa_lists:
8179
taxonomy_lists.append(
8280
(
@@ -255,9 +253,7 @@ def set_taxa_info_from_taxhub(taxhub_data, features):
255253
if feature["properties"]["cd_nom"] == taxon["cd_nom"]:
256254
excluded_keys = {"medias", "attributs"}
257255
filtered_data = {
258-
key: value
259-
for key, value in taxon.items()
260-
if key not in excluded_keys
256+
key: value for key, value in taxon.items() if key not in excluded_keys
261257
}
262258

263259
if (

0 commit comments

Comments
 (0)