diff --git a/VERSION b/VERSION index 7243b12cf4..68e69e405e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.14.2 +2.15.0 diff --git a/backend/dependencies/TaxHub b/backend/dependencies/TaxHub index 7d3808839c..9a51db8068 160000 --- a/backend/dependencies/TaxHub +++ b/backend/dependencies/TaxHub @@ -1 +1 @@ -Subproject commit 7d3808839c4ff0ef27fa0a19fa785a2a63ac4358 +Subproject commit 9a51db8068d8327e05a6f3cd8ae6733575b2825d diff --git a/backend/geonature/core/gn_synthese/routes.py b/backend/geonature/core/gn_synthese/routes.py index f9880af195..c5717a1498 100644 --- a/backend/geonature/core/gn_synthese/routes.py +++ b/backend/geonature/core/gn_synthese/routes.py @@ -21,7 +21,7 @@ from pypnnomenclature.models import BibNomenclaturesTypes, TNomenclatures from werkzeug.exceptions import Forbidden, NotFound, BadRequest, Conflict from werkzeug.datastructures import MultiDict -from sqlalchemy import distinct, func, desc, asc, select, case +from sqlalchemy import distinct, func, desc, asc, select, case, or_ from sqlalchemy.orm import joinedload, lazyload, selectinload, contains_eager from geojson import FeatureCollection, Feature import sqlalchemy as sa @@ -1529,7 +1529,18 @@ def list_all_reports(permissions): # Filter by id_role for 'pin' type only or if my_reports is true if type_name == "pin" or my_reports: - query = query.where(TReport.id_role == g.current_user.id_role) + query = query.where( + or_( + TReport.id_role == g.current_user.id_role, + TReport.id_synthese.in_( + select(TReport.id_synthese).where(TReport.id_role == g.current_user.id_role) + ), + TReport.synthese.has(Synthese.id_digitiser == g.current_user.id_role), + TReport.synthese.has( + Synthese.cor_observers.any(User.id_role == g.current_user.id_role) + ), + ) + ) # On vérifie les permissions en lecture sur la synthese synthese_query = select(Synthese.id_synthese).select_from(Synthese) diff --git a/backend/geonature/tests/fixtures.py b/backend/geonature/tests/fixtures.py index faab8949ab..c1972a1b22 100644 --- a/backend/geonature/tests/fixtures.py +++ b/backend/geonature/tests/fixtures.py @@ -989,8 +989,9 @@ def reports_data(users, synthese_data): data = [] # do not commit directly on current transaction, as we want to rollback all changes at the end of tests - def create_report(id_synthese, id_role, content, id_type, deleted): + def create_report(id_synthese, id_report, id_role, content, id_type, deleted): new_report = TReport( + id_report=id_report, id_synthese=id_synthese, id_role=id_role, content=content, @@ -1015,10 +1016,10 @@ def create_report(id_synthese, id_role, content, id_type, deleted): ) with db.session.begin_nested(): reports = [ - (ids[0], users["admin_user"].id_role, "comment1", discussionId, False), - (ids[1], users["admin_user"].id_role, "comment1", alertId, False), - (ids[2], users["user"].id_role, "a_comment1", discussionId, True), - (ids[3], users["user"].id_role, "b_comment1", discussionId, True), + (ids[0], 10001, users["admin_user"].id_role, "comment1", discussionId, False), + (ids[1], 10002, users["admin_user"].id_role, "comment1", alertId, False), + (ids[2], 10003, users["user"].id_role, "a_comment1", discussionId, True), + (ids[3], 10004, users["user"].id_role, "b_comment1", discussionId, True), ] for id_synthese, *args in reports: data.append(create_report(id_synthese, *args)) diff --git a/backend/geonature/tests/test_reports.py b/backend/geonature/tests/test_reports.py index 408e8560e3..d577693357 100644 --- a/backend/geonature/tests/test_reports.py +++ b/backend/geonature/tests/test_reports.py @@ -219,19 +219,23 @@ def test_list_all_reports( assert isinstance(response.json["items"], list) assert len(response.json["items"]) >= 0 - ids = [s.id_synthese for s in synthese_data.values()] # TEST WITH MY_REPORTS TRUE set_logged_user(self.client, users["user"]) response = self.client.get(url_for(url, type="discussion", my_reports="true")) assert response.status_code == 200 items = response.json["items"] - # Check that all items belong to the current user - id_role = users["user"].id_role - nom_complet = users["user"].nom_complet - assert all( - item["id_role"] == id_role and item["user"]["nom_complet"] == nom_complet - for item in items - ) + expected_ids = [ + 10001, # User is observer + 10003, # User is report owner + 10004, # User is report owner + ] + # Missing cases: + # - User is digitiser + # - User has post a report in the same synthese + # They involve adding data to the `synthese_data` fixture, which could cause other tests to fail. + item_ids = [item["id_report"] for item in items] + item_ids.sort() + assert expected_ids == item_ids # Test undefined type response = self.client.get(url_for(url, type="UNKNOW-REPORT-TYPE", my_reports="true")) diff --git a/backend/geonature/utils/config_schema.py b/backend/geonature/utils/config_schema.py index 1e83119c8e..4d21312bba 100644 --- a/backend/geonature/utils/config_schema.py +++ b/backend/geonature/utils/config_schema.py @@ -280,6 +280,7 @@ class TaxonSheet(Schema): # -------------------------------------------------------------------- # SYNTHESE - TAXON_SHEET ENABLE_PROFILE = fields.Boolean(load_default=True) + ENABLE_TAXONOMY = fields.Boolean(load_default=True) class Synthese(Schema): diff --git a/backend/requirements-dependencies.in b/backend/requirements-dependencies.in index 9af911e5c6..56c86a235c 100644 --- a/backend/requirements-dependencies.in +++ b/backend/requirements-dependencies.in @@ -3,5 +3,5 @@ pypnnomenclature>=1.6.4,<2 pypn_habref_api>=0.4.1,<1 utils-flask-sqlalchemy-geo>=0.3.2,<1 utils-flask-sqlalchemy>=0.4.1,<1 -taxhub==2.0.0rc1 +taxhub==2.0.0 pypn-ref-geo>=1.5.3,<2 diff --git a/backend/requirements.txt b/backend/requirements.txt index ea19f6831d..a59f9d0beb 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -31,6 +31,12 @@ bokeh==3.4.1 # via -r requirements-common.in brotli==1.1.0 # via fonttools +cairocffi==1.7.0 + # via + # cairosvg + # weasyprint +cairosvg==2.7.1 + # via weasyprint celery[redis]==5.4.0 # via -r requirements-common.in certifi==2024.8.30 @@ -301,7 +307,7 @@ sqlalchemy==1.4.54 # utils-flask-sqlalchemy # utils-flask-sqlalchemy-geo # wtforms-sqlalchemy -taxhub==2.0.0rc1 +taxhub==2.0.0 # via # -r requirements-dependencies.in # pypnnomenclature diff --git a/config/default_config.toml.example b/config/default_config.toml.example index 3e3418b622..012c784109 100644 --- a/config/default_config.toml.example +++ b/config/default_config.toml.example @@ -445,6 +445,8 @@ MEDIA_CLEAN_CRONTAB = "0 1 * * *" # Options dédiées à la fiche taxon # Permet d'activer ou non la section "Profile" ENABLE_PROFILE = true + # Permet d'activer ou non la section "Taxonomy" + ENABLE_TAXONOMY = true # Gestion des demandes d'inscription [ACCOUNT_MANAGEMENT] diff --git a/contrib/gn_module_validation/frontend/app/components/validation-modal-info-obs/validation-modal-info-obs.component.html b/contrib/gn_module_validation/frontend/app/components/validation-modal-info-obs/validation-modal-info-obs.component.html index 4d3086f59c..f39a1dc292 100644 --- a/contrib/gn_module_validation/frontend/app/components/validation-modal-info-obs/validation-modal-info-obs.component.html +++ b/contrib/gn_module_validation/frontend/app/components/validation-modal-info-obs/validation-modal-info-obs.component.html @@ -97,6 +97,7 @@
[mailCustomSubject]="config.VALIDATION.MAIL_SUBJECT" [mailCustomBody]="config.VALIDATION.MAIL_BODY" useFrom="validation" + [selectedTab]="tab" > - - - - diff --git a/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-info-obs/modal-info-obs.component.ts b/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-info-obs/modal-info-obs.component.ts deleted file mode 100644 index faec54615d..0000000000 --- a/frontend/src/app/syntheseModule/synthese-results/synthese-list/synthese-info-obs/modal-info-obs.component.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { SyntheseDataService } from '@geonature_common/form/synthese-form/synthese-data.service'; -import { DataFormService } from '@geonature_common/form/data-form.service'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { MediaService } from '@geonature_common/service/media.service'; -import { finalize } from 'rxjs/operators'; -import { ConfigService } from '@geonature/services/config.service'; - -@Component({ - selector: 'pnx-synthese-modal-info-obs', - templateUrl: 'modal-info-obs.component.html', -}) -export class ModalInfoObsComponent implements OnInit { - @Input() syntheseObs: any; - public selectedObs; - public selectedObsTaxonDetail; - public formatedAreas = []; - public SYNTHESE_CONFIG = null; - public isLoading = false; - constructor( - private _gnDataService: DataFormService, - private _dataService: SyntheseDataService, - public activeModal: NgbActiveModal, - public mediaService: MediaService, - public config: ConfigService - ) { - this.SYNTHESE_CONFIG = this.config.SYNTHESE; - } - - ngOnInit() { - this.loadOneSyntheseReleve(this.syntheseObs); - } - - loadOneSyntheseReleve(syntheseObs) { - this.isLoading = true; - this._dataService - .getOneSyntheseObservation(syntheseObs.id) - .pipe( - finalize(() => { - this.isLoading = false; - }) - ) - .subscribe((data) => { - this.selectedObs = data; - this.selectedObs['municipalities'] = []; - this.selectedObs['other_areas'] = []; - this.selectedObs['actors'] = this.selectedObs['actors'].split('|'); - const areaDict = {}; - // for each area type we want all the areas: we build an dict of array - this.selectedObs.areas.forEach((area) => { - if (!areaDict[area.area_type.type_name]) { - areaDict[area.area_type.type_name] = [area]; - } else { - areaDict[area.area_type.type_name].push(area); - } - }); - // for angular tempate we need to convert it into a aray - for (let key in areaDict) { - this.formatedAreas.push({ area_type: key, areas: areaDict[key] }); - } - - // this.inpnMapUrl = `https://inpn.mnhn.fr/cartosvg/couchegeo/repartition/atlas/${ - // this.selectedObs['cd_nom'] - // }/fr_light_l93,fr_light_mer_l93,fr_lit_l93)`; - }); - - const taxhubFields = ['attributs', 'attributs.bib_attribut.label_attribut', 'status']; - this._gnDataService.getTaxonInfo(syntheseObs.cd_nom, taxhubFields).subscribe((taxInfo) => { - this.selectedObsTaxonDetail = taxInfo; - // filter attributs - this.selectedObsTaxonDetail.attributs = taxInfo['attributs'].filter((v) => - this.config.SYNTHESE.ID_ATTRIBUT_TAXHUB.includes(v.id_attribut) - ); - }); - } - - backToModule(url_source, id_pk_source) { - window.open(url_source + '/' + id_pk_source, '_blank'); - } -} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/infos.component.html b/frontend/src/app/syntheseModule/taxon-sheet/infos/infos.component.html index 9608bc40c2..b2d722d056 100644 --- a/frontend/src/app/syntheseModule/taxon-sheet/infos/infos.component.html +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/infos.component.html @@ -1,18 +1,6 @@
-
-
- {{ taxon?.nom_complet }} -
-
- {{ taxon?.nom_vern }} -
-
+ +
+
Statuts
+
+ +
+
diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.scss b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.scss new file mode 100644 index 0000000000..8d2326bd7c --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.scss @@ -0,0 +1,16 @@ +// //////////////////////////////////////////////////////////////////////////// +// +// //////////////////////////////////////////////////////////////////////////// + +.Status { + display: flex; + flex-flow: row nowrap; + justify-content: flex-start; + align-items: center; + column-gap: 0.5rem; + line-height: 1; + + &__header { + vertical-align: middle; + } +} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.ts b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.ts new file mode 100644 index 0000000000..0cc562647b --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/status/status.component.ts @@ -0,0 +1,18 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { Taxon } from '@geonature_common/form/taxonomy/taxonomy.component'; +import { GN2CommonModule } from '@geonature_common/GN2Common.module'; + +@Component({ + standalone: true, + selector: 'status', + templateUrl: 'status.component.html', + styleUrls: ['status.component.scss'], + imports: [CommonModule, GN2CommonModule], +}) +export class StatusComponent { + constructor() {} + + @Input() + taxon: Taxon | null; +} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.html b/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.html new file mode 100644 index 0000000000..c74e9f299b --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.html @@ -0,0 +1,8 @@ +
+
+ {{ taxon?.nom_complet }} +
+
+ {{ taxon?.nom_vern }} +
+
diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.scss b/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.scss new file mode 100644 index 0000000000..4ac109a575 --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.scss @@ -0,0 +1,12 @@ +.Taxonomy { + display: flex; + flex-flow: column; + &__completeName { + font-weight: lighter; + font-style: italic; + } + &__vernacularName { + font-size: 1.5rem; + font-weight: bold; + } +} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.ts b/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.ts new file mode 100644 index 0000000000..27e3cbb63c --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/infos/taxonomy/taxonomy.component.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { Taxon } from '@geonature_common/form/taxonomy/taxonomy.component'; +@Component({ + standalone: true, + selector: 'taxonomy', + templateUrl: 'taxonomy.component.html', + styleUrls: ['taxonomy.component.scss'], + imports: [CommonModule], +}) +export class TaxonomyComponent { + @Input() + taxon: Taxon | null = null; +} diff --git a/frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.html b/frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.html new file mode 100644 index 0000000000..94da0dd149 --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.scss b/frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.ts b/frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.ts new file mode 100644 index 0000000000..e5767049de --- /dev/null +++ b/frontend/src/app/syntheseModule/taxon-sheet/tab-taxonomy/tab-taxonomy.component.ts @@ -0,0 +1,24 @@ +import { Component, OnInit } from '@angular/core'; +import { GN2CommonModule } from '@geonature_common/GN2Common.module'; +import { CommonModule } from '@angular/common'; +import { TaxonSheetService } from '../taxon-sheet.service'; +import { Taxon } from '@geonature_common/form/taxonomy/taxonomy.component'; +import { SharedSyntheseModule } from '@geonature/shared/syntheseSharedModule/synthese-shared.module'; + +@Component({ + standalone: true, + selector: 'tab-taxonomy', + templateUrl: 'tab-taxonomy.component.html', + styleUrls: ['tab-taxonomy.component.scss'], + imports: [GN2CommonModule, CommonModule, SharedSyntheseModule], +}) +export class TabTaxonomyComponent implements OnInit { + taxon: Taxon | null = null; + constructor(private _tss: TaxonSheetService) {} + + ngOnInit() { + this._tss.taxon.subscribe((taxon: Taxon | null) => { + this.taxon = taxon; + }); + } +} 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 66b098d819..b045a9e1c2 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 @@ -9,6 +9,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 { TabTaxonomyComponent } from './tab-taxonomy/tab-taxonomy.component'; interface Tab { label: string; @@ -24,6 +25,12 @@ export const ALL_TAXON_SHEET_ADVANCED_INFOS_ROUTES: Array = [ component: TabGeographicOverviewComponent, configEnabledField: null, // make it always available ! }, + { + label: 'Taxonomie', + path: 'taxonomy', + configEnabledField: 'ENABLE_TAXONOMY', + component: TabTaxonomyComponent, + }, { label: 'Profil', path: 'profile', diff --git a/install/assets/db/add_pg_extensions.sql b/install/assets/db/add_pg_extensions.sql index 1fac8f7d84..d2050e9818 100644 --- a/install/assets/db/add_pg_extensions.sql +++ b/install/assets/db/add_pg_extensions.sql @@ -2,6 +2,7 @@ CREATE EXTENSION IF NOT EXISTS "hstore"; CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "pg_trgm"; CREATE EXTENSION IF NOT EXISTS "unaccent"; +CREATE EXTENSION IF NOT EXISTS "ltree"; -- postgis