From 08f9bd0acf15eee65768cb3da421e32ad81e29dc Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Tue, 17 Dec 2024 15:20:55 +0100 Subject: [PATCH 01/28] List species for hedges --- envergo/hedges/models.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/envergo/hedges/models.py b/envergo/hedges/models.py index 6013abce7..44ec6fe2b 100644 --- a/envergo/hedges/models.py +++ b/envergo/hedges/models.py @@ -1,12 +1,22 @@ import operator import uuid +<<<<<<< HEAD from collections import defaultdict from functools import reduce +||||||| parent of 58df3585 (List species for hedges) +======= +from functools import reduce +>>>>>>> 58df3585 (List species for hedges) from django.contrib.postgres.fields import ArrayField from django.db import models +<<<<<<< HEAD from django.db.models import Q from model_utils import Choices +||||||| parent of 58df3585 (List species for hedges) +======= +from django.db.models import Q +>>>>>>> 58df3585 (List species for hedges) from pyproj import Geod from shapely import LineString @@ -86,6 +96,7 @@ def connexion_boisement(self): def sous_ligne_electrique(self): return self.additionalData.get("sousLigneElectrique", None) +<<<<<<< HEAD def get_species_filter(self): """Build the filter to get possible protected species. @@ -116,6 +127,33 @@ def get_species(self): qs = Species.objects.filter(self.get_species_filter()) return qs +||||||| parent of 58df3585 (List species for hedges) +======= + def get_species_filter(self): + q_hedge_type = Q(hedge_types__contains=[self.hedge_type]) + + exclude = [] + + if not self.proximite_mare: + exclude.append(Q(proximite_mare=True)) + if not self.vieil_arbre: + exclude.append(Q(vieil_arbre=True)) + if not self.proximite_point_eau: + exclude.append(Q(proximite_point_eau=True)) + if not self.connexion_boisement: + exclude.append(Q(connexion_boisement=True)) + + q_exclude = reduce(operator.or_, exclude) + filter = q_hedge_type & ~q_exclude + return filter + + def get_species(self): + """Return known specis in this hedge.""" + + qs = Species.objects.filter(self.get_species_filter()) + return qs + +>>>>>>> 58df3585 (List species for hedges) class HedgeData(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) From a30ec2b9b1c6feb535838caa2ce2631edcb29ab3 Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Fri, 10 Jan 2025 11:18:12 +0100 Subject: [PATCH 02/28] Store map attributes as a json --- .../migrations/0017_zone_attributes.py | 20 +++++++++++++++++++ envergo/geodata/models.py | 1 + envergo/geodata/utils.py | 4 ++++ 3 files changed, 25 insertions(+) create mode 100644 envergo/geodata/migrations/0017_zone_attributes.py diff --git a/envergo/geodata/migrations/0017_zone_attributes.py b/envergo/geodata/migrations/0017_zone_attributes.py new file mode 100644 index 000000000..117348d35 --- /dev/null +++ b/envergo/geodata/migrations/0017_zone_attributes.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.13 on 2025-01-10 10:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("geodata", "0016_alter_department_geometry"), + ] + + operations = [ + migrations.AddField( + model_name="zone", + name="attributes", + field=models.JSONField( + blank=True, null=True, verbose_name="Entity attributes" + ), + ), + ] diff --git a/envergo/geodata/models.py b/envergo/geodata/models.py index 441a1f782..ccbc0809e 100644 --- a/envergo/geodata/models.py +++ b/envergo/geodata/models.py @@ -131,6 +131,7 @@ class Zone(gis_models.Model): area = models.BigIntegerField(_("Area"), null=True, blank=True) npoints = models.BigIntegerField(_("Number of points"), null=True, blank=True) created_at = models.DateTimeField(_("Date created"), default=timezone.now) + attributes = models.JSONField(_("Entity attributes"), null=True, blank=True) class Meta: verbose_name = _("Zone") diff --git a/envergo/geodata/utils.py b/envergo/geodata/utils.py index 20ef25938..54e5eba25 100644 --- a/envergo/geodata/utils.py +++ b/envergo/geodata/utils.py @@ -64,6 +64,10 @@ def __init__(self, *args, **kwargs): def feature_kwargs(self, feat): kwargs = super().feature_kwargs(feat) kwargs.update(self.extra_kwargs) + + fields = feat.fields + entities = {f: feat.get(f) for f in fields} + kwargs["entities"] = entities return kwargs From face12aa04d27754824d5ad9a045091b137ab523 Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Fri, 10 Jan 2025 11:19:49 +0100 Subject: [PATCH 03/28] Create new "species" map type --- .../migrations/0018_alter_map_map_type.py | 27 +++++++++++++++++++ envergo/geodata/models.py | 1 + 2 files changed, 28 insertions(+) create mode 100644 envergo/geodata/migrations/0018_alter_map_map_type.py diff --git a/envergo/geodata/migrations/0018_alter_map_map_type.py b/envergo/geodata/migrations/0018_alter_map_map_type.py new file mode 100644 index 000000000..a882ed9b7 --- /dev/null +++ b/envergo/geodata/migrations/0018_alter_map_map_type.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.13 on 2025-01-10 10:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("geodata", "0017_zone_attributes"), + ] + + operations = [ + migrations.AlterField( + model_name="map", + name="map_type", + field=models.CharField( + blank=True, + choices=[ + ("zone_humide", "Zone humide"), + ("zone_inondable", "Zone inondable"), + ("species", "Espèces protégées"), + ], + max_length=50, + verbose_name="Map type", + ), + ), + ] diff --git a/envergo/geodata/models.py b/envergo/geodata/models.py index ccbc0809e..6eb1bb3b7 100644 --- a/envergo/geodata/models.py +++ b/envergo/geodata/models.py @@ -24,6 +24,7 @@ MAP_TYPES = Choices( ("zone_humide", _("Zone humide")), ("zone_inondable", _("Zone inondable")), + ("species", _("Espèces protégées")), ) # Sometimes, there are map with different certainty values. From b7ae90c57404054dafa839b8de36d7e50e213961 Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Fri, 10 Jan 2025 12:06:57 +0100 Subject: [PATCH 04/28] Use the appropriate field name in import --- envergo/geodata/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/envergo/geodata/utils.py b/envergo/geodata/utils.py index 54e5eba25..43a112e7e 100644 --- a/envergo/geodata/utils.py +++ b/envergo/geodata/utils.py @@ -66,8 +66,8 @@ def feature_kwargs(self, feat): kwargs.update(self.extra_kwargs) fields = feat.fields - entities = {f: feat.get(f) for f in fields} - kwargs["entities"] = entities + attributes = {f: feat.get(f) for f in fields} + kwargs["attributes"] = attributes return kwargs From 8ee436f0d008ea528c608b23ff263860bb4556d2 Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Fri, 10 Jan 2025 12:07:22 +0100 Subject: [PATCH 05/28] Filter species by location --- envergo/hedges/models.py | 80 +++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/envergo/hedges/models.py b/envergo/hedges/models.py index 44ec6fe2b..fc007ffd8 100644 --- a/envergo/hedges/models.py +++ b/envergo/hedges/models.py @@ -1,25 +1,18 @@ import operator import uuid -<<<<<<< HEAD from collections import defaultdict from functools import reduce -||||||| parent of 58df3585 (List species for hedges) -======= -from functools import reduce ->>>>>>> 58df3585 (List species for hedges) +from django.contrib.gis.geos import Polygon from django.contrib.postgres.fields import ArrayField from django.db import models -<<<<<<< HEAD from django.db.models import Q from model_utils import Choices -||||||| parent of 58df3585 (List species for hedges) -======= -from django.db.models import Q ->>>>>>> 58df3585 (List species for hedges) from pyproj import Geod from shapely import LineString +from envergo.geodata.models import Zone + TO_PLANT = "TO_PLANT" TO_REMOVE = "TO_REMOVE" @@ -96,7 +89,6 @@ def connexion_boisement(self): def sous_ligne_electrique(self): return self.additionalData.get("sousLigneElectrique", None) -<<<<<<< HEAD def get_species_filter(self): """Build the filter to get possible protected species. @@ -127,33 +119,6 @@ def get_species(self): qs = Species.objects.filter(self.get_species_filter()) return qs -||||||| parent of 58df3585 (List species for hedges) -======= - def get_species_filter(self): - q_hedge_type = Q(hedge_types__contains=[self.hedge_type]) - - exclude = [] - - if not self.proximite_mare: - exclude.append(Q(proximite_mare=True)) - if not self.vieil_arbre: - exclude.append(Q(vieil_arbre=True)) - if not self.proximite_point_eau: - exclude.append(Q(proximite_point_eau=True)) - if not self.connexion_boisement: - exclude.append(Q(connexion_boisement=True)) - - q_exclude = reduce(operator.or_, exclude) - filter = q_hedge_type & ~q_exclude - return filter - - def get_species(self): - """Return known specis in this hedge.""" - - qs = Species.objects.filter(self.get_species_filter()) - return qs - ->>>>>>> 58df3585 (List species for hedges) class HedgeData(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) @@ -170,6 +135,20 @@ def __str__(self): def __iter__(self): return iter(self.hedges()) + def get_bounding_box(self): + """Return the bounding box of the whole hedge set.""" + + hedges = self.hedges() + min_x, min_y, max_x, max_y = hedges[0].geometry.bounds + for hedge in hedges[1:]: + x0, y0, x1, y1 = hedge.geometry.bounds + min_x = min(min_x, x0) + min_y = min(min_y, y0) + max_x = max(max_x, x1) + max_y = max(max_y, y1) + box = Polygon.from_bbox([min_x, min_y, max_x, max_y]) + return box + def hedges(self): return [Hedge(**h) for h in self.data] @@ -245,13 +224,28 @@ def get_lengths_to_plant(self): "alignement": lengths_by_type["alignement"], } + def get_local_species_names(self): + """Return species names that reported to live here.""" + + bbox = self.get_bounding_box() + zones = Zone.objects.filter(geometry__intersects=bbox).filter( + map__map_type="species" + ) + zone_species = set() + for zone in zones: + # XXX + # the map file will be updated + # the fild will be renamed "especes" + # the field value will be an array + zone_species.update(zone.attributes.get("espece", "").split(",")) + return zone_species + def get_all_species(self): - """Return all species in the set of hedges.""" + """Return the local list of protected species.""" - filters = [h.get_species_filter() for h in self.hedges_to_remove()] - union = reduce(operator.or_, filters) - qs = Species.objects.filter(union).order_by("group", "common_name") - return qs + hedge_species_qs = self.get_hedge_species() + local_species_names = self.get_local_species_names() + return hedge_species_qs.filter(common_name__in=local_species_names) HEDGE_TYPES = ( From 41e3031eccacf98e4fb6952d817d394ea291bcd3 Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Mon, 13 Jan 2025 14:58:58 +0100 Subject: [PATCH 06/28] Import Aisne species data --- .../migrations/0006_import_aisne_species.py | 51 +++++++++++++ .../migrations/0007_alter_species_group.py | 30 ++++++++ envergo/hedges/migrations/ep_aisne.csv | 75 +++++++++++++++++++ envergo/hedges/models.py | 2 +- 4 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 envergo/hedges/migrations/0006_import_aisne_species.py create mode 100644 envergo/hedges/migrations/0007_alter_species_group.py create mode 100644 envergo/hedges/migrations/ep_aisne.csv diff --git a/envergo/hedges/migrations/0006_import_aisne_species.py b/envergo/hedges/migrations/0006_import_aisne_species.py new file mode 100644 index 000000000..aed76e417 --- /dev/null +++ b/envergo/hedges/migrations/0006_import_aisne_species.py @@ -0,0 +1,51 @@ +# Generated by Django 4.2.13 on 2025-01-13 13:31 +import csv + +from django.db import migrations +from django.template.defaultfilters import slugify + +HEDGE_TYPES = dict( + ( + ("degradee", "Haie dégradée ou résiduelle basse"), + ("buissonnante", "Haie buissonnante basse"), + ("arbustive", "Haie arbustive"), + ("alignement", "Alignement d'arbres"), + ("mixte", "Haie mixte"), + ) +).keys() + + +def import_aisne_species(apps, schema_editor): + Species = apps.get_model("hedges", "Species") + with open("envergo/hedges/migrations/ep_aisne.csv") as f: + reader = csv.DictReader(f) + for row in reader: + hedges = [] + for key in HEDGE_TYPES: + if row[key] == "True": + hedges.append(key) + Species.objects.create( + group=slugify(row["group"]), + common_name=row["common_name"], + scientific_name=row["scientific_name"], + level_of_concern=row["level_of_concern"], + highly_sensitive=row["highly_sensitive"] == "True", + proximite_mare=row["proximite_mare"] == "True", + proximite_point_eau=row["proximite_point_eau"] == "True", + connexion_boisement=row["connexion_boisement"] == "True", + vieil_arbre=row["vieil_arbre"] == "True", + hedge_types=hedges, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("hedges", "0005_rename_highly_sentitive_species_highly_sensitive"), + ] + + operations = [ + migrations.RunPython( + import_aisne_species, reverse_code=migrations.RunPython.noop + ) + ] diff --git a/envergo/hedges/migrations/0007_alter_species_group.py b/envergo/hedges/migrations/0007_alter_species_group.py new file mode 100644 index 000000000..67390d64d --- /dev/null +++ b/envergo/hedges/migrations/0007_alter_species_group.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.13 on 2025-01-13 13:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("hedges", "0006_import_aisne_species"), + ] + + operations = [ + migrations.AlterField( + model_name="species", + name="group", + field=models.CharField( + choices=[ + ("amphibiens", "Amphibiens"), + ("chauves-souris", "Chauves-souris"), + ("flore", "Flore"), + ("insectes", "Insectes"), + ("mammiferes-terrestres", "Mammifères terrestres"), + ("oiseaux", "Oiseaux"), + ("reptile", "Reptile"), + ], + max_length=64, + verbose_name="Groupe", + ), + ), + ] diff --git a/envergo/hedges/migrations/ep_aisne.csv b/envergo/hedges/migrations/ep_aisne.csv new file mode 100644 index 000000000..93ae1e9f0 --- /dev/null +++ b/envergo/hedges/migrations/ep_aisne.csv @@ -0,0 +1,75 @@ +group,common_name,scientific_name,level_of_concern,highly_sensitive,degradee,buissonnante,arbustive,alignement,mixte,proximite_mare,vieil_arbre,proximite_point_eau,connexion_boisement +Amphibiens,Rainette verte,Hyla arborea,fort,,True,True,True,True,True,True,,, +Amphibiens,Triton crêté,Triturus cristatus,fort,,True,True,True,True,True,True,,, +Chauves-souris,Barbastelle d’Europe,Barbastella barbastellus,tres_fort,True,,,,True,True,,True,, +Chauves-souris,Grand murin,Myotis myotis,fort,,,,,True,True,,True,, +Chauves-souris,Grand rhinolophe,Rhinolophus ferrumequinum,fort,,,,,True,True,,True,, +Chauves-souris,Murin à moustaches,Myotis mystacinus,moyen,,,,,True,True,,True,, +Chauves-souris,Murin de Bechstein,Myotis bechsteinii,fort,,,,,True,True,,True,, +Chauves-souris,Murin de Daubenton,Myotis daubentonii,moyen,,,,,True,True,,True,, +Chauves-souris,Noctule commune,Nyctalus noctula,fort,,,,,True,True,,True,, +Chauves-souris,Noctule de Leisler,Nyctalus leisleri,fort,,,,,True,True,,True,, +Chauves-souris,Oreillard roux,Plecotus auritus,fort,,,,,True,True,,True,, +Chauves-souris,Petit rhinolophe,Rhinolophus hipposideros,fort,,,,,True,True,,True,, +Chauves-souris,Pipistrelle commune,Pipistrellus pipistrellus,moyen,,,,,True,True,,True,, +Chauves-souris,Sérotine commune,Eptesicus serotinus,moyen,,,,,True,True,,True,, +Flore,Lathrée écailleuse,Lathraea squamaria,fort,True,,,,True,True,,,True, +Flore,Nivéole printanière,Leucojum vernum,fort,True,True,True,True,True,True,,,, +Flore,Orme lisse,Ulmus laevis,tres_fort,True,,,,True,True,,,, +Flore,Raiponce noire,Phyteuma nigrum,fort,True,True,True,True,True,True,,,,True +Insectes,Pique prune,Osmoderma eremita,moyen,,,,,True,True,,True,, +Mammifères terrestres,Écureuil roux,Sciurus vulgaris,faible,,,,,True,True,,,, +Mammifères terrestres,Hérisson d’Europe,Erinaceus europaeus,faible,,True,True,True,True,True,,,, +Mammifères terrestres,Muscardin,Muscardinus avellanarius,moyen,,True,True,True,True,True,,,, +Oiseaux,Accenteur mouchet,Prunella Modularis,faible,,True,True,True,True,True,,,, +Oiseaux,Bouvreuil pivoine,Pyrrhula pyrrhula,moyen,,,,,True,True,,,, +Oiseaux,Bruant jaune,Emberiza citrinella,moyen,,True,True,True,True,True,,,, +Oiseaux,Bruant zizi,Emberiza cirlus,fort,,,,True,True,True,,,, +Oiseaux,Buse variable,Buteo buteo,faible,,,,,True,,,,, +Oiseaux,Chardonneret élégant,Carduelis carduelis,moyen,,,,True,,True,,,, +Oiseaux,Chevêche d’Athéna,Athene noctua,fort,,,,,True,,,,, +Oiseaux,Chouette hulotte,Strix aluco,faible,,,,,True,,,,, +Oiseaux,Coucou gris,Cuculus canorus,faible,,,,True,,True,,,, +Oiseaux,Épervier d’Europe,Accipiter nisus,faible,,,,,True,True,,,, +Oiseaux,Faucon crécerelle,Falco tinnuculus,faible,,,,,True,True,,,, +Oiseaux,Faucon hobereau,Falco subbuteo,moyen,,,,,True,,,,, +Oiseaux,Fauvette à tête noire,Sylvia atricapilla,faible,,,,True,True,True,,,, +Oiseaux,Fauvette babillarde,Curruca curruca,faible,,,,True,,True,,,, +Oiseaux,Fauvette des jardins,Sylvia borin,faible,,,,True,,True,,,, +Oiseaux,Fauvette grisette,Sylvia communis,faible,,True,True,True,,,,,, +Oiseaux,Gobemouche gris,Muscicapa striapa,faible,,,,,True,,,,, +Oiseaux,Hibou moyen-duc,Asio otus,faible,,,,,True,,,,, +Oiseaux,Huppe fasciée,Upupa epops,majeur,True,,,,True,True,,,, +Oiseaux,Hypolaïs ictérine,Hippolais icterina,tres_fort,True,,,True,True,True,,,, +Oiseaux,Hypolaïs polyglotte,Hipolais polyglotta,faible,,,,True,,True,,,, +Oiseaux,Linotte mélodieuse,Carduelis cannabina,moyen,,True,True,True,True,True,,,, +Oiseaux,Loriot d’Europe,Oriolus oriolus,faible,,,,,True,True,,,, +Oiseaux,Mésange à longue queue,Aegithalos caudatus,faible,,,,True,True,True,,,, +Oiseaux,Mésange bleue,Cyanistes caeruleus,faible,,,,,True,True,,,, +Oiseaux,Mésange boréale,Poecile montanus,moyen,,,,,True,True,,,, +Oiseaux,Mésange charbonnière,Parus major,faible,,,,,True,True,,,, +Oiseaux,Mésange nonnette,Poecile palustris,faible,,,,,True,True,,,, +Oiseaux,Moineau friquet,Passer montanus,fort,,,,,True,True,,,, +Oiseaux,Pic épeiche,Dendrocopos major,faible,,,,,True,True,,,, +Oiseaux,Pic épeichette,Dendrocopos minor,moyen,,,,,True,True,,,, +Oiseaux,Pic mar,Dendrocopos medius,moyen,,,,,True,True,,,, +Oiseaux,Pic vert,Picus viridis,faible,,,,,True,True,,,, +Oiseaux,Pie-grièche écorcheur,Lanius collurio,fort,,True,,True,,True,,,, +Oiseaux,Pie-grièche grise,Lanius excubitor,majeur,True,,,True,True,True,,,, +Oiseaux,Pinson des arbres,Fringilla coelebs,faible,,,,True,True,True,,,, +Oiseaux,Pipit des arbres,Anthus trivialis,faible,,,,,True,True,,,, +Oiseaux,Pipit farlouse,Anthus pratensis,moyen,,True,,,,,,,, +Oiseaux,Pouillot fitis,Phylloscopus trochilus,moyen,,,,True,,True,,,, +Oiseaux,Pouillot véloce,Phylloscopus collybita,faible,,,,,True,True,,,, +Oiseaux,Rossignol philomèle,Luscinia megarhynchos,faible,,,,True,,True,,,, +Oiseaux,Rougegorge familier,Erithacus rubicula,faible,,True,True,True,True,True,,,, +Oiseaux,Rougequeue à fronc blanc,Phoenicurus phoenicurus,moyen,,,,,True,True,,,, +Oiseaux,Sitelle torchepot,Sitta europaea,faible,,,,,True,True,,,, +Oiseaux,Tarier pâtre,Saxicola rubicola,moyen,,True,True,True,,,,,, +Oiseaux,Torcol fourmilier,Jynx torquilla,tres_fort,True,,,,True,True,,,, +Oiseaux,Troglodyte mignon,Troglodytes troglodytes,faible,,,,,True,True,,,, +Oiseaux,Verdier d’Europe,Chloris chloris,faible,,,,,True,True,,,, +Reptile,Couleuvre à collier,Natrix natrix,moyen,,,True,True,True,True,,,, +Reptile,Couleuvre d’Esculape,Zamensis longissimus,moyen,,,,True,True,True,,,, +Reptile,Lézard vivipare,Zootoca vivipare,moyen,,True,True,True,True,True,,,, +Reptile,Orvet fragile,Anguis fragilis,faible,,True,True,True,True,True,,,, diff --git a/envergo/hedges/models.py b/envergo/hedges/models.py index fc007ffd8..970933a6b 100644 --- a/envergo/hedges/models.py +++ b/envergo/hedges/models.py @@ -261,7 +261,7 @@ def get_all_species(self): ("chauves-souris", "Chauves-souris"), ("flore", "Flore"), ("insectes", "Insectes"), - ("mammifères-terrestres", "Mammifères terrestres"), + ("mammiferes-terrestres", "Mammifères terrestres"), ("oiseaux", "Oiseaux"), ("reptile", "Reptile"), ) From b2e82a47a45a61491afe9da8c6af232b4c46f7da Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Mon, 13 Jan 2025 15:01:40 +0100 Subject: [PATCH 07/28] Improve the species admin module Update columns order, are more search and filters possibilities. --- envergo/hedges/admin.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/envergo/hedges/admin.py b/envergo/hedges/admin.py index 3e7ed091a..5c9de70ab 100644 --- a/envergo/hedges/admin.py +++ b/envergo/hedges/admin.py @@ -94,13 +94,14 @@ class SpeciesAdminForm(forms.ModelForm): @admin.register(Species) class SpeciesAdmin(admin.ModelAdmin): list_display = [ + "group", "common_name", "scientific_name", - "group", - "hedge_types", "level_of_concern", "highly_sensitive", + "hedge_types", ] + search_fields = ["group", "common_name", "scientific_name"] ordering = ["-common_name"] form = SpeciesAdminForm - list_filter = ["group"] + list_filter = ["group", "level_of_concern", "highly_sensitive"] From 0102a92c21ab7e557307f6db6d24283bb719b5dc Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Tue, 14 Jan 2025 15:23:49 +0100 Subject: [PATCH 08/28] Fix rebase mistake --- envergo/hedges/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/envergo/hedges/models.py b/envergo/hedges/models.py index 970933a6b..289cab421 100644 --- a/envergo/hedges/models.py +++ b/envergo/hedges/models.py @@ -224,6 +224,14 @@ def get_lengths_to_plant(self): "alignement": lengths_by_type["alignement"], } + def get_hedge_species(self): + """Return species that may live in the hedges.""" + + filters = [h.get_species_filter() for h in self.hedges_to_remove()] + union = reduce(operator.or_, filters) + species = Species.objects.filter(union).order_by("group", "common_name") + return species + def get_local_species_names(self): """Return species names that reported to live here.""" From 0cf34e49d696dd542bc5c01f1e5e74d583b3c2b7 Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Tue, 14 Jan 2025 15:45:50 +0100 Subject: [PATCH 09/28] Fix an issue with empty filters --- envergo/hedges/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/envergo/hedges/models.py b/envergo/hedges/models.py index 289cab421..ca33dd6f9 100644 --- a/envergo/hedges/models.py +++ b/envergo/hedges/models.py @@ -109,8 +109,10 @@ def get_species_filter(self): if not self.connexion_boisement: exclude.append(Q(connexion_boisement=True)) - q_exclude = reduce(operator.or_, exclude) - filter = q_hedge_type & ~q_exclude + filter = q_hedge_type + if exclude: + q_exclude = reduce(operator.or_, exclude) + filter &= ~q_exclude return filter def get_species(self): From 36b02a0a6906d54bdb34bd0d027f0b719baf8988 Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Thu, 16 Jan 2025 11:50:50 +0100 Subject: [PATCH 10/28] Fix the species extraction and filtering --- envergo/geodata/utils.py | 19 ++++++++++++++++++- envergo/hedges/models.py | 8 ++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/envergo/geodata/utils.py b/envergo/geodata/utils.py index 43a112e7e..86224f03a 100644 --- a/envergo/geodata/utils.py +++ b/envergo/geodata/utils.py @@ -65,11 +65,28 @@ def feature_kwargs(self, feat): kwargs = super().feature_kwargs(feat) kwargs.update(self.extra_kwargs) + # We extract shapefile attributes to the `attributes` json model field fields = feat.fields - attributes = {f: feat.get(f) for f in fields} + attributes = {f: self.get_attribute(feat, f) for f in fields} kwargs["attributes"] = attributes return kwargs + def get_attribute(self, feat, field): + """Extract shapefile attribute. + + We can define custom methods for specific fields. + """ + if f"get_attribute_{field}" in dir(self): + attr = getattr(self, f"get_attribute_{field}")(feat) + else: + attr = feat.get(field) + return attr + + def get_attribute_especes(self, feat): + raw_especes = feat.get("especes") + especes = [e.strip('"') for e in raw_especes.split(",")] + return especes + @contextmanager def extract_shapefile(archive): diff --git a/envergo/hedges/models.py b/envergo/hedges/models.py index ca33dd6f9..1560a23e0 100644 --- a/envergo/hedges/models.py +++ b/envergo/hedges/models.py @@ -235,7 +235,7 @@ def get_hedge_species(self): return species def get_local_species_names(self): - """Return species names that reported to live here.""" + """Return species names that are known to live here.""" bbox = self.get_bounding_box() zones = Zone.objects.filter(geometry__intersects=bbox).filter( @@ -243,11 +243,7 @@ def get_local_species_names(self): ) zone_species = set() for zone in zones: - # XXX - # the map file will be updated - # the fild will be renamed "especes" - # the field value will be an array - zone_species.update(zone.attributes.get("espece", "").split(",")) + zone_species.update(zone.attributes.get("especes", [])) return zone_species def get_all_species(self): From 921e824c8906f6e9aebf9b16b4824f21bf73c563 Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Mon, 20 Jan 2025 10:23:10 +0100 Subject: [PATCH 11/28] Integrate species into criterion result --- envergo/moulinette/regulations/ep.py | 57 +++++++++++++- envergo/templates/hedges/_species_table.html | 31 ++++++++ .../ep/ep_aisne_derogation_inventaire.html | 77 +++++++++++++++++++ .../ep/ep_aisne_derogation_simplifiee.html | 31 ++++++++ .../moulinette/ep/ep_aisne_interdit.html | 52 +++++++++++++ .../moulinette/ep/ep_simple_soumis.html | 7 ++ .../templates/moulinette/ep/ep_soumis.html | 46 ----------- 7 files changed, 254 insertions(+), 47 deletions(-) create mode 100644 envergo/templates/hedges/_species_table.html create mode 100644 envergo/templates/moulinette/ep/ep_aisne_derogation_inventaire.html create mode 100644 envergo/templates/moulinette/ep/ep_aisne_derogation_simplifiee.html create mode 100644 envergo/templates/moulinette/ep/ep_aisne_interdit.html create mode 100644 envergo/templates/moulinette/ep/ep_simple_soumis.html delete mode 100644 envergo/templates/moulinette/ep/ep_soumis.html diff --git a/envergo/moulinette/regulations/ep.py b/envergo/moulinette/regulations/ep.py index f1ffffc94..e70349102 100644 --- a/envergo/moulinette/regulations/ep.py +++ b/envergo/moulinette/regulations/ep.py @@ -1,8 +1,11 @@ +from envergo.evaluations.models import RESULTS from envergo.moulinette.regulations import CriterionEvaluator class EspecesProtegees(CriterionEvaluator): - choice_label = "EP > EP" + """Legacy criterion for protected species.""" + + choice_label = "EP > EP (obsolète)" slug = "ep" CODE_MATRIX = { @@ -18,3 +21,55 @@ def get_catalog_data(self): def get_result_data(self): return "soumis" + + +class EspecesProtegeesSimple(EspecesProtegees): + """Basic criterion: always returns "soumis.""" + + choice_label = "EP > EP simple" + slug = "ep_simple" + + +class EspecesProtegeesAisne(CriterionEvaluator): + """Check for protected species living in hedges.""" + + choice_label = "EP > EP Aisne" + slug = "ep_aisne" + + CODE_MATRIX = { + (False, True): "interdit", + (False, False): "interdit", + (True, True): "derogation_inventaire", + (True, False): "derogation_simplifiee", + } + + RESULT_MATRIX = { + "interdit": RESULTS.interdit, + "derogation_inventaire": RESULTS.soumis, + "derogation_simplifiee": RESULTS.soumis, + } + + def get_catalog_data(self): + catalog = super().get_catalog_data() + haies = self.catalog.get("haies") + if haies: + species = haies.get_all_species() + catalog["protected_species"] = species + catalog["fauna_sensitive_species"] = [ + s for s in species if s.highly_sensitive + ] + catalog["flora_sensitive_species"] = [ + s for s in species if s.highly_sensitive + ] + return catalog + + def get_result_data(self): + has_reimplantation = self.catalog.get("reimplantation") != "non" + has_sensitive_species = False + species = self.catalog.get("protected_species") + for s in species: + if s.highly_sensitive: + has_sensitive_species = True + break + + return has_reimplantation, has_sensitive_species diff --git a/envergo/templates/hedges/_species_table.html b/envergo/templates/hedges/_species_table.html new file mode 100644 index 000000000..a1e258f24 --- /dev/null +++ b/envergo/templates/hedges/_species_table.html @@ -0,0 +1,31 @@ +
+
+
+
+ + + + + + + + + + + + {% for s in species %} + + + + + + + {% endfor %} + +
Liste des espèces protégées potentiellement présentes
GroupeNom communNom latinNiveau d'enjeu
{{ s.get_group_display }}{{ s.common_name }} + {{ s.scientific_name }} + {{ s.get_level_of_concern_display }}
+
+
+
+
diff --git a/envergo/templates/moulinette/ep/ep_aisne_derogation_inventaire.html b/envergo/templates/moulinette/ep/ep_aisne_derogation_inventaire.html new file mode 100644 index 000000000..c6620c4da --- /dev/null +++ b/envergo/templates/moulinette/ep/ep_aisne_derogation_inventaire.html @@ -0,0 +1,77 @@ +

+ Le projet est soumis à la réglementation sur les espèces protégées. Pour être autorisé, il doit obtenir une dérogation des services de l’État. +

+ +

+ ⚠️ Un inventaire de terrain est peut-être nécessaire pour recenser certaines espèces présentes dans les haies détruites. En effet, au vu des caractéristiques du projet, des espèces particulièrement sensibles sont susceptibles de s’y trouver. +

+ +

+ Avant de déposer sa demande, le porteur de projet doit contacter la ou les structures listées ci-dessous. Leur rôle est de déterminer, au vu du projet, si un inventaire de terrain ciblé sur certaines espèces est nécessaire ou non. +

+ +{% if fauna_sensitive_species %} +

Concernant les espèces animales suivantes :

+ +
    + {% for species in fauna_sensitive_species %}
  • {{ species.common_name }}
  • {% endfor %} +
+ +
+ Picardie Nature +
+ contact@picardie-nature.org +
+ 03 62 72 22 50 +
+{% endif %} + +{% if flora_sensitive_species %} +

Concernant les espèces florales suivantes :

+ +
    + {% for species in flora_sensitive_species %}
  • {{ species.common_name }}
  • {% endfor %} +
+ +
+ Conservatoire botanique national de Bailleul +
+ jc.hauguel@cbnbl.org +
+ 07 85 85 15 96 +
+{% endif %} + +

+ Si l’inventaire n’est pas nécessaire, la structure fournira une attestation en ce sens, qui devra être jointe au dossier de demande d’autorisation déposé au guichet unique de la haie. +

+ +

+ Si un inventaire ciblé est nécessaire, un bureau d’études naturaliste devra être missionné pour le réaliser. Les conclusions de l’étude devront être jointes au dossier de demande d’autorisation. +

+ +{% include 'moulinette/_read_more_btn.html' with aria_controls=criterion.unique_slug %} + +
+ +

+ Vous pouvez consulter la procédure simplifiée mise en place dans l’Aisne pour l’application de la réglementation sur les espèces protégées (voir page 33). +

+ +

Pour obtenir une dérogation, le porteur de projet devra justifier dans son dossier :

+ +
    +
  • de la raison pour laquelle la haie est détruite ;
  • +
  • de l’absence de solution alternative qui permettrait d’éviter la destruction ;
  • +
  • + des mesures mises en œuvre pour réduire, et à défaut pour compenser, les impacts du projet sur les espèces et les habitats abrités par les haies détruites. +
  • +
+ + {% if protected_species %} + {% include 'hedges/_species_table.html' with species=protected_species %} + {% endif %} + +
diff --git a/envergo/templates/moulinette/ep/ep_aisne_derogation_simplifiee.html b/envergo/templates/moulinette/ep/ep_aisne_derogation_simplifiee.html new file mode 100644 index 000000000..33a75e2bc --- /dev/null +++ b/envergo/templates/moulinette/ep/ep_aisne_derogation_simplifiee.html @@ -0,0 +1,31 @@ +

+ Le projet est soumis à la réglementation sur les espèces protégées. Pour être autorisé, il doit obtenir une dérogation des services de l’État. +

+ +

+ Au vu des caractéristiques des haies détruites, une procédure simplifiée peut être suivie : l’inventaire de terrain pour recenser les espèces présentes dans les haies détruites n’est pas nécessaire. +

+ +

Pour obtenir une dérogation, le porteur de projet devra justifier dans son dossier :

+ +
    +
  • de la raison pour laquelle la haie est détruite ;
  • +
  • de l’absence de solution alternative qui permettrait d’éviter la destruction ;
  • +
  • + des mesures mises en œuvre pour réduire, et à défaut pour compenser, les impacts du projet sur les espèces et les habitats abrités par les haies détruites. +
  • +
+ +{% include 'moulinette/_read_more_btn.html' with aria_controls=criterion.unique_slug %} + +
+

+ Vous pouvez consulter la procédure simplifiée mise en place dans l’Aisne pour l’application de la réglementation sur les espèces protégées (voir page 33). +

+ + {% if protected_species %} + {% include 'hedges/_species_table.html' with species=protected_species %} + {% endif %} +
diff --git a/envergo/templates/moulinette/ep/ep_aisne_interdit.html b/envergo/templates/moulinette/ep/ep_aisne_interdit.html new file mode 100644 index 000000000..ef67b1077 --- /dev/null +++ b/envergo/templates/moulinette/ep/ep_aisne_interdit.html @@ -0,0 +1,52 @@ +

+ Le projet est interdit au titre de la réglementation sur les espèces protégées car il ne prévoit pas de plantation d’une haie en compensation des impacts engendrés. +

+ +

Options pour rendre le projet autorisable :

+ +
    +
  • prévoir de planter une autre haie
  • +
+ +{% if not is_read_only %} +

+ Vous pouvez modifier votre projet dans le simulateur. +

+{% endif %} + +{% include 'moulinette/_read_more_btn.html' with aria_controls=criterion.unique_slug %} + +
+ +

+ Tout projet de destruction de haie est soumis à la réglementation sur les espèces protégées. Pour être autorisé, il doit obtenir une dérogation des services de l’État. +

+ +

Pour qu’une dérogation puisse être délivrée, le porteur de projet doit prouver :

+ +
    +
  • que le projet est réalisé pour des motifs impérieux ;
  • +
  • + que toutes les mesures ont été prises pour éviter les impacts du projet sur les espèces protégées (réduire le linéaire détruit ou détruire les haies les moins riches en biodiversité) ; +
  • +
  • et en dernier lieu, que les impacts inévitables sont compensés.
  • +
+ +

+ La plantation d’une haie en remplacement de la haie détruite est donc nécessaire en guise de compensation des impacts résiduels du projet. +

+ +

+ En modifiant votre projet et en répondant « oui, en plantant une haie à un autre endroit », ce simulateur vous indiquera les caractéristiques de la haie à planter nécessaires pour que le projet soit susceptible d’être autorisé par l’administration. +

+ +

+ D’autres mesures de compensation incluent la plantation d’arbres ou de bosquets, ou la restauration de haies existantes et leur gestion vertueuse pour les espèces : maintien de bandes enherbées, réduction de la fréquence de taille… +

+ +

+ Vous pouvez consulter la procédure simplifiée mise en place dans l’Aisne pour l’application de la réglementation sur les espèces protégées (à partir de la page 33, et page 52 pour les mesures de compensation). +

+
diff --git a/envergo/templates/moulinette/ep/ep_simple_soumis.html b/envergo/templates/moulinette/ep/ep_simple_soumis.html new file mode 100644 index 000000000..fbbb5bfbe --- /dev/null +++ b/envergo/templates/moulinette/ep/ep_simple_soumis.html @@ -0,0 +1,7 @@ +

Le projet est soumis à demande de dérogation espèces protégées

+ +{% if protected_species %} + {% include 'hedges/_species_table.html' with species=protected_species %} +{% else %} +

Aucune espèce protégée trouvée.

+{% endif %} diff --git a/envergo/templates/moulinette/ep/ep_soumis.html b/envergo/templates/moulinette/ep/ep_soumis.html deleted file mode 100644 index 8b14cf739..000000000 --- a/envergo/templates/moulinette/ep/ep_soumis.html +++ /dev/null @@ -1,46 +0,0 @@ -

Le projet est soumis à demande de dérogation espèces protégées

- -{% if protected_species %} -
-

- -

-
-
-
-
-
- - - - - - - - - - - - {% for species in protected_species %} - - - - - - - {% endfor %} - -
Liste des espèces protégées potentiellement présentes
GroupeNom communNom latinNiveau d'enjeu
{{ species.get_group_display }}{{ species.common_name }} - {{ species.scientific_name }} - {{ species.get_level_of_concern_display }}
-
-
-
-
-
-
-{% else %} -

Aucune espèce protégée trouvée.

-{% endif %} From 366d1b6416d292657e39bd18cc503154901799b3 Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Mon, 20 Jan 2025 15:44:38 +0100 Subject: [PATCH 12/28] Handle the geopackage format --- envergo/geodata/admin.py | 20 +++++++-------- envergo/geodata/models.py | 18 +------------- envergo/geodata/tasks.py | 6 ++--- envergo/geodata/utils.py | 52 ++++++++++++++++++++++++--------------- 4 files changed, 46 insertions(+), 50 deletions(-) diff --git a/envergo/geodata/admin.py b/envergo/geodata/admin.py index 7c53ee858..2eb1d4da8 100644 --- a/envergo/geodata/admin.py +++ b/envergo/geodata/admin.py @@ -13,13 +13,15 @@ from envergo.geodata.forms import DepartmentForm from envergo.geodata.models import Department, Map, Zone -from envergo.geodata.tasks import generate_map_preview, process_shapefile_map -from envergo.geodata.utils import count_features, extract_shapefile +from envergo.geodata.tasks import generate_map_preview, process_map +from envergo.geodata.utils import count_features, extract_map class MapForm(forms.ModelForm): def clean_file(self): - """Check that the given file is a valid shapefile archive. + """Check that the given file is a valid map. + + We handle two formats : shapefile and geopackage. The official shapefile format is just a bunch of files with the same name and different extensions. @@ -29,7 +31,7 @@ def clean_file(self): """ file = self.cleaned_data["file"] try: - with extract_shapefile(file): + with extract_map(file): pass # This file is valid, yeah \o/ except Exception as e: raise ValidationError(_(f"This file does not seem valid ({e})")) @@ -96,7 +98,7 @@ def get_search_results(self, request, queryset, search_term): return queryset, may_have_duplicates def save_model(self, request, obj, form, change): - obj.expected_zones = count_features(obj.file) + obj.expected_zones = count_features(obj.file.file) super().save_model(request, obj, form, change) def get_queryset(self, request): @@ -195,7 +197,7 @@ def col_zones(self, obj): return f'{imported} / {obj.expected_zones or ""}' - @admin.action(description=_("Extract and import a shapefile")) + @admin.action(description=_("Extract and import a map (.shp / gpkg)")) def process(self, request, queryset): if queryset.count() > 1: error = _("Please only select one map for this action.") @@ -203,10 +205,8 @@ def process(self, request, queryset): return map = queryset[0] - process_shapefile_map.delay(map.id) - msg = _( - "Your shapefile will be processed soon. It might take up to a few minutes." - ) + process_map.delay(map.id) + msg = _("Your map will be processed soon. It might take up to a few minutes.") self.message_user(request, msg, level=messages.INFO) @admin.action(description=_("Generate the simplified preview geometry")) diff --git a/envergo/geodata/models.py b/envergo/geodata/models.py index 6eb1bb3b7..3c6c883c0 100644 --- a/envergo/geodata/models.py +++ b/envergo/geodata/models.py @@ -1,8 +1,4 @@ -import glob import logging -import zipfile -from contextlib import contextmanager -from tempfile import TemporaryDirectory from django.contrib.gis.db import models as gis_models from django.contrib.postgres.fields import ArrayField @@ -43,7 +39,7 @@ class Map(models.Model): - """Holds a shapefile map.""" + """Holds a map file (shapefile / gpkg).""" name = models.CharField(_("Name"), max_length=256) display_name = models.CharField(_("Display name"), max_length=256, blank=True) @@ -104,18 +100,6 @@ class Meta: def __str__(self): return self.name - @contextmanager - def extract_shapefile(self): - with TemporaryDirectory() as tmpdir: - logger.info("Extracting map zip file") - zf = zipfile.ZipFile(self.file) - zf.extractall(tmpdir) - - logger.info("Find .shp file path") - paths = glob.glob(f"{tmpdir}/*shp") # glop glop ! - shapefile = paths[0] - yield shapefile - class Zone(gis_models.Model): """Stores an annotated geographic polygon(s).""" diff --git a/envergo/geodata/tasks.py b/envergo/geodata/tasks.py index d94cae267..9af63918a 100644 --- a/envergo/geodata/tasks.py +++ b/envergo/geodata/tasks.py @@ -5,14 +5,14 @@ from config.celery_app import app from envergo.geodata.models import STATUSES, Map -from envergo.geodata.utils import make_polygons_valid, process_shapefile, simplify_map +from envergo.geodata.utils import make_polygons_valid, process_map_file, simplify_map logger = logging.getLogger(__name__) @app.task(bind=True) @transaction.atomic -def process_shapefile_map(task, map_id): +def process_map(task, map_id): logger.info(f"Starting import on map {map_id}") map = Map.objects.get(pk=map_id) @@ -28,7 +28,7 @@ def process_shapefile_map(task, map_id): try: with transaction.atomic(): map.zones.all().delete() - process_shapefile(map, map.file, task) + process_map_file(map, map.file, task) make_polygons_valid(map) map.geometry = simplify_map(map) except Exception as e: diff --git a/envergo/geodata/utils.py b/envergo/geodata/utils.py index 86224f03a..eabf6c49a 100644 --- a/envergo/geodata/utils.py +++ b/envergo/geodata/utils.py @@ -65,14 +65,14 @@ def feature_kwargs(self, feat): kwargs = super().feature_kwargs(feat) kwargs.update(self.extra_kwargs) - # We extract shapefile attributes to the `attributes` json model field + # We extract map attributes to the `attributes` json model field fields = feat.fields attributes = {f: self.get_attribute(feat, f) for f in fields} kwargs["attributes"] = attributes return kwargs def get_attribute(self, feat, field): - """Extract shapefile attribute. + """Extract map attribute. We can define custom methods for specific fields. """ @@ -89,29 +89,41 @@ def get_attribute_especes(self, feat): @contextmanager -def extract_shapefile(archive): - """Extract a shapefile from a zip archive.""" +def extract_map(archive): + """Returns the path to the map file. - with TemporaryDirectory() as tmpdir: - logger.info("Extracting map zip file") - zf = zipfile.ZipFile(archive) - zf.extractall(tmpdir) + If this is a zipped shapefile, extract it to a temporary directory. + """ + if archive.name.endswith(".zip"): + with TemporaryDirectory() as tmpdir: + logger.info("Extracting map zip file") + zf = zipfile.ZipFile(archive) + zf.extractall(tmpdir) - logger.info("Find .shp file path") - paths = glob.glob(f"{tmpdir}/*shp") # glop glop ! + logger.info("Find .shp file path") + paths = glob.glob(f"{tmpdir}/*shp") # glop glop ! - try: - shapefile = paths[0] - except IndexError: - raise ValueError(_("No .shp file found in archive")) + try: + shapefile = paths[0] + except IndexError: + raise ValueError(_("No .shp file found in archive")) + + yield shapefile - yield shapefile + elif archive.name.endswith(".gpkg"): + if hasattr(archive, "temporary_file_path"): + yield archive.temporary_file_path() + else: + yield archive.path + + else: + raise ValueError(_("Unsupported file format")) -def count_features(shapefile): +def count_features(map_file): """Count the number of features from a shapefile.""" - with extract_shapefile(shapefile) as file: + with extract_map(map_file) as file: ds = DataSource(file) layer = ds[0] nb_zones = len(layer) @@ -119,9 +131,9 @@ def count_features(shapefile): return nb_zones -def process_shapefile(map, file, task=None): +def process_map_file(map, file, task=None): logger.info("Creating temporary directory") - with extract_shapefile(file) as shapefile: + with extract_map(file) as map_file: if task: debug_stream = CeleryDebugStream(task, map.expected_zones) else: @@ -132,7 +144,7 @@ def process_shapefile(map, file, task=None): extra = {"map": map} lm = CustomMapping( Zone, - shapefile, + map_file, mapping, transaction_mode="autocommit", extra_kwargs=extra, From b2556b4595a58ba75cb19d3fb1e21ff3af7b926a Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Mon, 20 Jan 2025 15:44:49 +0100 Subject: [PATCH 13/28] Make sure attributes field is read only --- envergo/geodata/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envergo/geodata/admin.py b/envergo/geodata/admin.py index 2eb1d4da8..8e4c7e7f5 100644 --- a/envergo/geodata/admin.py +++ b/envergo/geodata/admin.py @@ -272,7 +272,7 @@ class ZoneAdmin(gis_admin.GISModelAdmin): "area", "npoints", ] - readonly_fields = ["map", "created_at", "area", "npoints"] + readonly_fields = ["map", "created_at", "area", "npoints", "attributes"] list_filter = ["map__map_type", "map__data_type"] # Prevent an expensive count query From dc0bd6ee90eb6adac1534de02ba85a963f47e46c Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Tue, 21 Jan 2025 10:29:22 +0100 Subject: [PATCH 14/28] Reorder admin columns --- envergo/hedges/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envergo/hedges/admin.py b/envergo/hedges/admin.py index 5c9de70ab..3b48f25e5 100644 --- a/envergo/hedges/admin.py +++ b/envergo/hedges/admin.py @@ -94,9 +94,9 @@ class SpeciesAdminForm(forms.ModelForm): @admin.register(Species) class SpeciesAdmin(admin.ModelAdmin): list_display = [ - "group", "common_name", "scientific_name", + "group", "level_of_concern", "highly_sensitive", "hedge_types", From f2026793092f99b96e947a63027edef85f573c2d Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Wed, 22 Jan 2025 16:31:44 +0100 Subject: [PATCH 15/28] Add taxref id to Species class --- envergo/hedges/admin.py | 2 ++ envergo/hedges/management/__init__.py | 0 .../hedges/management/commands/__init__.py | 0 .../migrations/0008_species_taxref_ids.py | 24 +++++++++++++++++++ envergo/hedges/models.py | 10 ++++++++ 5 files changed, 36 insertions(+) create mode 100644 envergo/hedges/management/__init__.py create mode 100644 envergo/hedges/management/commands/__init__.py create mode 100644 envergo/hedges/migrations/0008_species_taxref_ids.py diff --git a/envergo/hedges/admin.py b/envergo/hedges/admin.py index 3b48f25e5..2aaca4205 100644 --- a/envergo/hedges/admin.py +++ b/envergo/hedges/admin.py @@ -100,8 +100,10 @@ class SpeciesAdmin(admin.ModelAdmin): "level_of_concern", "highly_sensitive", "hedge_types", + "taxref_ids", ] search_fields = ["group", "common_name", "scientific_name"] ordering = ["-common_name"] form = SpeciesAdminForm list_filter = ["group", "level_of_concern", "highly_sensitive"] + readonly_fields = ["taxref_ids"] diff --git a/envergo/hedges/management/__init__.py b/envergo/hedges/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/envergo/hedges/management/commands/__init__.py b/envergo/hedges/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/envergo/hedges/migrations/0008_species_taxref_ids.py b/envergo/hedges/migrations/0008_species_taxref_ids.py new file mode 100644 index 000000000..3355fe255 --- /dev/null +++ b/envergo/hedges/migrations/0008_species_taxref_ids.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.13 on 2025-01-22 08:48 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("hedges", "0007_alter_species_group"), + ] + + operations = [ + migrations.AddField( + model_name="species", + name="taxref_ids", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.IntegerField(), + null=True, + size=None, + verbose_name="Ids TaxRef (cd_nom)", + ), + ), + ] diff --git a/envergo/hedges/models.py b/envergo/hedges/models.py index 1560a23e0..f7627090c 100644 --- a/envergo/hedges/models.py +++ b/envergo/hedges/models.py @@ -284,6 +284,16 @@ def get_all_species(self): class Species(models.Model): """Represent a single species.""" + # This is the unique species identifier (cd_nom) in the INPN TaxRef database + # https://inpn.mnhn.fr/telechargement/referentielEspece/referentielTaxo + # The reason why this is an array is because sometimes, there are duplicates + # (e.g) a species has been describe by several naturalists over the years before + # they realized it was a duplicate. + # Hence, for a given scientific name, there can be several TaxRef ids. + taxref_ids = ArrayField( + null=True, verbose_name="Ids TaxRef (cd_nom)", base_field=models.IntegerField() + ) + # This "group" is an ad-hoc category, not related to the official biology taxonomy group = models.CharField("Groupe", choices=SPECIES_GROUPS, max_length=64) common_name = models.CharField("Nom commun", max_length=255) From c9f444d88ee95bc52ccbedcb240d0d99393912cd Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Wed, 22 Jan 2025 16:32:03 +0100 Subject: [PATCH 16/28] Fix bad species names in aisne species list --- envergo/hedges/migrations/ep_aisne.csv | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/envergo/hedges/migrations/ep_aisne.csv b/envergo/hedges/migrations/ep_aisne.csv index 93ae1e9f0..7c6b26d81 100644 --- a/envergo/hedges/migrations/ep_aisne.csv +++ b/envergo/hedges/migrations/ep_aisne.csv @@ -21,7 +21,7 @@ Insectes,Pique prune,Osmoderma eremita,moyen,,,,,True,True,,True,, Mammifères terrestres,Écureuil roux,Sciurus vulgaris,faible,,,,,True,True,,,, Mammifères terrestres,Hérisson d’Europe,Erinaceus europaeus,faible,,True,True,True,True,True,,,, Mammifères terrestres,Muscardin,Muscardinus avellanarius,moyen,,True,True,True,True,True,,,, -Oiseaux,Accenteur mouchet,Prunella Modularis,faible,,True,True,True,True,True,,,, +Oiseaux,Accenteur mouchet,Prunella modularis,faible,,True,True,True,True,True,,,, Oiseaux,Bouvreuil pivoine,Pyrrhula pyrrhula,moyen,,,,,True,True,,,, Oiseaux,Bruant jaune,Emberiza citrinella,moyen,,True,True,True,True,True,,,, Oiseaux,Bruant zizi,Emberiza cirlus,fort,,,,True,True,True,,,, @@ -31,17 +31,17 @@ Oiseaux,Chevêche d’Athéna,Athene noctua,fort,,,,,True,,,,, Oiseaux,Chouette hulotte,Strix aluco,faible,,,,,True,,,,, Oiseaux,Coucou gris,Cuculus canorus,faible,,,,True,,True,,,, Oiseaux,Épervier d’Europe,Accipiter nisus,faible,,,,,True,True,,,, -Oiseaux,Faucon crécerelle,Falco tinnuculus,faible,,,,,True,True,,,, +Oiseaux,Faucon crécerelle,Falco tinnunculus,faible,,,,,True,True,,,, Oiseaux,Faucon hobereau,Falco subbuteo,moyen,,,,,True,,,,, Oiseaux,Fauvette à tête noire,Sylvia atricapilla,faible,,,,True,True,True,,,, Oiseaux,Fauvette babillarde,Curruca curruca,faible,,,,True,,True,,,, Oiseaux,Fauvette des jardins,Sylvia borin,faible,,,,True,,True,,,, Oiseaux,Fauvette grisette,Sylvia communis,faible,,True,True,True,,,,,, -Oiseaux,Gobemouche gris,Muscicapa striapa,faible,,,,,True,,,,, +Oiseaux,Gobemouche gris,Muscicapa striata,faible,,,,,True,,,,, Oiseaux,Hibou moyen-duc,Asio otus,faible,,,,,True,,,,, Oiseaux,Huppe fasciée,Upupa epops,majeur,True,,,,True,True,,,, Oiseaux,Hypolaïs ictérine,Hippolais icterina,tres_fort,True,,,True,True,True,,,, -Oiseaux,Hypolaïs polyglotte,Hipolais polyglotta,faible,,,,True,,True,,,, +Oiseaux,Hyppolaïs polyglotte,Hippolais polyglotta,faible,,,,True,,True,,,, Oiseaux,Linotte mélodieuse,Carduelis cannabina,moyen,,True,True,True,True,True,,,, Oiseaux,Loriot d’Europe,Oriolus oriolus,faible,,,,,True,True,,,, Oiseaux,Mésange à longue queue,Aegithalos caudatus,faible,,,,True,True,True,,,, @@ -62,7 +62,7 @@ Oiseaux,Pipit farlouse,Anthus pratensis,moyen,,True,,,,,,,, Oiseaux,Pouillot fitis,Phylloscopus trochilus,moyen,,,,True,,True,,,, Oiseaux,Pouillot véloce,Phylloscopus collybita,faible,,,,,True,True,,,, Oiseaux,Rossignol philomèle,Luscinia megarhynchos,faible,,,,True,,True,,,, -Oiseaux,Rougegorge familier,Erithacus rubicula,faible,,True,True,True,True,True,,,, +Oiseaux,Rougegorge familier,Erithacus rubecula,faible,,True,True,True,True,True,,,, Oiseaux,Rougequeue à fronc blanc,Phoenicurus phoenicurus,moyen,,,,,True,True,,,, Oiseaux,Sitelle torchepot,Sitta europaea,faible,,,,,True,True,,,, Oiseaux,Tarier pâtre,Saxicola rubicola,moyen,,True,True,True,,,,,, @@ -70,6 +70,6 @@ Oiseaux,Torcol fourmilier,Jynx torquilla,tres_fort,True,,,,True,True,,,, Oiseaux,Troglodyte mignon,Troglodytes troglodytes,faible,,,,,True,True,,,, Oiseaux,Verdier d’Europe,Chloris chloris,faible,,,,,True,True,,,, Reptile,Couleuvre à collier,Natrix natrix,moyen,,,True,True,True,True,,,, -Reptile,Couleuvre d’Esculape,Zamensis longissimus,moyen,,,,True,True,True,,,, -Reptile,Lézard vivipare,Zootoca vivipare,moyen,,True,True,True,True,True,,,, +Reptile,Couleuvre d’Esculape,Zamenis longissimus,moyen,,,,True,True,True,,,, +Reptile,Lézard vivipare,Zootoca vivipara,moyen,,True,True,True,True,True,,,, Reptile,Orvet fragile,Anguis fragilis,faible,,True,True,True,True,True,,,, From 46038b09e73b85f4629b87fef411486fb6875de7 Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Wed, 22 Jan 2025 16:32:22 +0100 Subject: [PATCH 17/28] Import taxref ids from taxref file --- .../management/commands/import_taxref.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 envergo/hedges/management/commands/import_taxref.py diff --git a/envergo/hedges/management/commands/import_taxref.py b/envergo/hedges/management/commands/import_taxref.py new file mode 100644 index 000000000..5ec31a10a --- /dev/null +++ b/envergo/hedges/management/commands/import_taxref.py @@ -0,0 +1,63 @@ +import csv +import glob +import io +import os +import zipfile +from tempfile import TemporaryDirectory + +import requests +from django.core.management.base import BaseCommand + +from envergo.hedges.models import Species + +# Download link can be found here +# https://inpn.mnhn.fr/telechargement/referentielEspece/referentielTaxo +TAXREF_URL = "https://inpn.mnhn.fr/docs-web/docs/download/454260" + + +class Command(BaseCommand): + help = "Update species data with TaxRef identifiers (cd_nom)." + + def add_arguments(self, parser): + parser.add_argument("taxref_url", type=str, nargs="?", default=TAXREF_URL) + + def handle(self, *args, **options): + + # Read the taxref file + taxref_url = options["taxref_url"] + if os.path.exists(taxref_url): + with open(taxref_url, "rb") as f: + file_content = io.BytesIO(f.read()) + else: + r = requests.get(taxref_url, stream=True) + file_content = io.BytesIO(r.content) + + with TemporaryDirectory() as tmpdir: + zf = zipfile.ZipFile(file_content) + zf.extractall(tmpdir) + + paths = glob.glob(f"{tmpdir}/TAXREF*.txt") + try: + path = paths[0] + except IndexError: + self.stderr.write(self.style.ERROR("No TAXREF file found")) + return + + # Reset taxref ids for all species + Species.objects.update(taxref_ids=[]) + species_names = Species.objects.all().values_list( + "scientific_name", flat=True + ) + + with open(path) as csvfile: + reader = csv.DictReader(csvfile, delimiter="\t") + for row in reader: + scientific_name = row["LB_NOM"] + vernacular_name_id = row["CD_NOM"] + if scientific_name in species_names: + # AFAIK, there is still no way to update an array field in a + # sigle query. Issue open since 9 years + # https://code.djangoproject.com/ticket/26355 + species = Species.objects.get(scientific_name=scientific_name) + species.taxref_ids.append(vernacular_name_id) + species.save() From bd6f95c24a99ea6c0203ef98b9a7cc027d9005d8 Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Thu, 23 Jan 2025 10:01:16 +0100 Subject: [PATCH 18/28] Update the species extraction Species are now a list of numbers, corresponding to the `cd_nom` taxref field. --- envergo/geodata/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envergo/geodata/utils.py b/envergo/geodata/utils.py index eabf6c49a..046097aee 100644 --- a/envergo/geodata/utils.py +++ b/envergo/geodata/utils.py @@ -84,7 +84,7 @@ def get_attribute(self, feat, field): def get_attribute_especes(self, feat): raw_especes = feat.get("especes") - especes = [e.strip('"') for e in raw_especes.split(",")] + especes = list(map(int, raw_especes.split(","))) return especes From 402b74281f2bcc137e7339e0d5bc659f617b7523 Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Thu, 23 Jan 2025 10:13:09 +0100 Subject: [PATCH 19/28] Use code to filter local species --- envergo/hedges/models.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/envergo/hedges/models.py b/envergo/hedges/models.py index f7627090c..dc1a959a6 100644 --- a/envergo/hedges/models.py +++ b/envergo/hedges/models.py @@ -234,24 +234,24 @@ def get_hedge_species(self): species = Species.objects.filter(union).order_by("group", "common_name") return species - def get_local_species_names(self): + def get_local_species_codes(self): """Return species names that are known to live here.""" bbox = self.get_bounding_box() zones = Zone.objects.filter(geometry__intersects=bbox).filter( map__map_type="species" ) - zone_species = set() + codes = set() for zone in zones: - zone_species.update(zone.attributes.get("especes", [])) - return zone_species + codes.update(zone.attributes.get("especes", [])) + return list(codes) def get_all_species(self): """Return the local list of protected species.""" hedge_species_qs = self.get_hedge_species() - local_species_names = self.get_local_species_names() - return hedge_species_qs.filter(common_name__in=local_species_names) + local_species_codes = self.get_local_species_codes() + return hedge_species_qs.filter(taxref_ids__overlap=local_species_codes) HEDGE_TYPES = ( From 216d70da7184cfa1b690937b86c862f2038869a9 Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Thu, 23 Jan 2025 16:10:31 +0100 Subject: [PATCH 20/28] Handle url case --- envergo/geodata/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/envergo/geodata/utils.py b/envergo/geodata/utils.py index 046097aee..54b802141 100644 --- a/envergo/geodata/utils.py +++ b/envergo/geodata/utils.py @@ -113,6 +113,8 @@ def extract_map(archive): elif archive.name.endswith(".gpkg"): if hasattr(archive, "temporary_file_path"): yield archive.temporary_file_path() + elif hasattr(archive, "url"): + yield archive.url else: yield archive.path From 8c1569fa49d904f149c44322f044267654fdc1c6 Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Fri, 24 Jan 2025 08:59:45 +0100 Subject: [PATCH 21/28] Add unique constraint on species scientific name --- .../0009_alter_species_scientific_name.py | 20 +++++++++++++++++++ envergo/hedges/models.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 envergo/hedges/migrations/0009_alter_species_scientific_name.py diff --git a/envergo/hedges/migrations/0009_alter_species_scientific_name.py b/envergo/hedges/migrations/0009_alter_species_scientific_name.py new file mode 100644 index 000000000..4616e64bd --- /dev/null +++ b/envergo/hedges/migrations/0009_alter_species_scientific_name.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.17 on 2025-01-24 07:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("hedges", "0008_species_taxref_ids"), + ] + + operations = [ + migrations.AlterField( + model_name="species", + name="scientific_name", + field=models.CharField( + max_length=255, unique=True, verbose_name="Nom scientifique" + ), + ), + ] diff --git a/envergo/hedges/models.py b/envergo/hedges/models.py index dc1a959a6..13c714312 100644 --- a/envergo/hedges/models.py +++ b/envergo/hedges/models.py @@ -297,7 +297,7 @@ class Species(models.Model): # This "group" is an ad-hoc category, not related to the official biology taxonomy group = models.CharField("Groupe", choices=SPECIES_GROUPS, max_length=64) common_name = models.CharField("Nom commun", max_length=255) - scientific_name = models.CharField("Nom scientifique", max_length=255) + scientific_name = models.CharField("Nom scientifique", max_length=255, unique=True) level_of_concern = models.CharField( "Niveau d'enjeu", max_length=16, choices=LEVELS_OF_CONCERN ) From 3ace210857d2bd8f4698e5a0da841ab8a722b9cc Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Fri, 24 Jan 2025 12:17:40 +0100 Subject: [PATCH 22/28] Save kingdom in species model --- envergo/hedges/admin.py | 2 +- .../management/commands/import_taxref.py | 2 ++ .../hedges/migrations/0010_species_kingdom.py | 31 +++++++++++++++++++ envergo/hedges/models.py | 12 +++++++ 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 envergo/hedges/migrations/0010_species_kingdom.py diff --git a/envergo/hedges/admin.py b/envergo/hedges/admin.py index 2aaca4205..c3611c6dd 100644 --- a/envergo/hedges/admin.py +++ b/envergo/hedges/admin.py @@ -106,4 +106,4 @@ class SpeciesAdmin(admin.ModelAdmin): ordering = ["-common_name"] form = SpeciesAdminForm list_filter = ["group", "level_of_concern", "highly_sensitive"] - readonly_fields = ["taxref_ids"] + readonly_fields = ["kingdom", "taxref_ids"] diff --git a/envergo/hedges/management/commands/import_taxref.py b/envergo/hedges/management/commands/import_taxref.py index 5ec31a10a..47800d288 100644 --- a/envergo/hedges/management/commands/import_taxref.py +++ b/envergo/hedges/management/commands/import_taxref.py @@ -54,10 +54,12 @@ def handle(self, *args, **options): for row in reader: scientific_name = row["LB_NOM"] vernacular_name_id = row["CD_NOM"] + kingdom = row["REGNE"].lower() if scientific_name in species_names: # AFAIK, there is still no way to update an array field in a # sigle query. Issue open since 9 years # https://code.djangoproject.com/ticket/26355 species = Species.objects.get(scientific_name=scientific_name) species.taxref_ids.append(vernacular_name_id) + species.kingdom = kingdom species.save() diff --git a/envergo/hedges/migrations/0010_species_kingdom.py b/envergo/hedges/migrations/0010_species_kingdom.py new file mode 100644 index 000000000..945a71f1f --- /dev/null +++ b/envergo/hedges/migrations/0010_species_kingdom.py @@ -0,0 +1,31 @@ +# Generated by Django 4.2.17 on 2025-01-24 08:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("hedges", "0009_alter_species_scientific_name"), + ] + + operations = [ + migrations.AddField( + model_name="species", + name="kingdom", + field=models.CharField( + blank=True, + choices=[ + ("animalia", "Animalia"), + ("archaea", "Archaea"), + ("bacteria", "Bacteria"), + ("chromista", "Chromista"), + ("fungi", "Fungi"), + ("plantae", "Plantae"), + ("protozoa", "Protozoa"), + ], + max_length=32, + verbose_name="Règne", + ), + ), + ] diff --git a/envergo/hedges/models.py b/envergo/hedges/models.py index 13c714312..93567029d 100644 --- a/envergo/hedges/models.py +++ b/envergo/hedges/models.py @@ -272,6 +272,16 @@ def get_all_species(self): ("reptile", "Reptile"), ) +KINGDOMS = Choices( + ("animalia", "Animalia"), + ("archaea", "Archaea"), + ("bacteria", "Bacteria"), + ("chromista", "Chromista"), + ("fungi", "Fungi"), + ("plantae", "Plantae"), + ("protozoa", "Protozoa"), +) + LEVELS_OF_CONCERN = Choices( ("faible", "Faible"), ("moyen", "Moyen"), @@ -296,6 +306,8 @@ class Species(models.Model): # This "group" is an ad-hoc category, not related to the official biology taxonomy group = models.CharField("Groupe", choices=SPECIES_GROUPS, max_length=64) + + kingdom = models.CharField("Règne", choices=KINGDOMS, max_length=32, blank=True) common_name = models.CharField("Nom commun", max_length=255) scientific_name = models.CharField("Nom scientifique", max_length=255, unique=True) level_of_concern = models.CharField( From 6a97a86b2822fb054aa5c681636d2687e4bc62bb Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Fri, 24 Jan 2025 12:17:50 +0100 Subject: [PATCH 23/28] Filter contact data by species kingdom --- envergo/moulinette/regulations/ep.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/envergo/moulinette/regulations/ep.py b/envergo/moulinette/regulations/ep.py index e70349102..5ac0f9a3e 100644 --- a/envergo/moulinette/regulations/ep.py +++ b/envergo/moulinette/regulations/ep.py @@ -56,10 +56,10 @@ def get_catalog_data(self): species = haies.get_all_species() catalog["protected_species"] = species catalog["fauna_sensitive_species"] = [ - s for s in species if s.highly_sensitive + s for s in species if s.highly_sensitive and s.kingdom == "animalia" ] catalog["flora_sensitive_species"] = [ - s for s in species if s.highly_sensitive + s for s in species if s.highly_sensitive and s.kingdom == "plantae" ] return catalog From 0b82137cff878fe720eab2f4eb2ad88e9db5172a Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Fri, 24 Jan 2025 12:34:52 +0100 Subject: [PATCH 24/28] Fix issue in the file parsing pattern --- envergo/hedges/management/commands/import_taxref.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envergo/hedges/management/commands/import_taxref.py b/envergo/hedges/management/commands/import_taxref.py index 47800d288..e6c2c73a6 100644 --- a/envergo/hedges/management/commands/import_taxref.py +++ b/envergo/hedges/management/commands/import_taxref.py @@ -36,7 +36,7 @@ def handle(self, *args, **options): zf = zipfile.ZipFile(file_content) zf.extractall(tmpdir) - paths = glob.glob(f"{tmpdir}/TAXREF*.txt") + paths = glob.glob(f"{tmpdir}/TAXREFv*.txt") try: path = paths[0] except IndexError: From b6672d76f8092800a636fd8fe756d97a641e242e Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Fri, 24 Jan 2025 12:36:02 +0100 Subject: [PATCH 25/28] Fix ep criterion result label --- envergo/evaluations/models.py | 2 ++ envergo/moulinette/models.py | 2 ++ envergo/moulinette/regulations/ep.py | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/envergo/evaluations/models.py b/envergo/evaluations/models.py index 8c1b0348a..016ac8ce1 100644 --- a/envergo/evaluations/models.py +++ b/envergo/evaluations/models.py @@ -97,6 +97,8 @@ def params_from_url(url): "non_active", "Non disponible", ), # Same message for users, but we need to separate `non_active` and `non_disponible` + ("derogation_inventaire", "Dérogation"), + ("derogation_simplifiee", "Dérogation simplifiée"), ) diff --git a/envergo/moulinette/models.py b/envergo/moulinette/models.py index 705114846..ac4a79afb 100644 --- a/envergo/moulinette/models.py +++ b/envergo/moulinette/models.py @@ -75,6 +75,8 @@ RESULTS.systematique, RESULTS.cas_par_cas, RESULTS.soumis, + RESULTS.derogation_inventaire, + RESULTS.derogation_simplifiee, RESULTS.action_requise, RESULTS.a_verifier, RESULTS.iota_a_verifier, diff --git a/envergo/moulinette/regulations/ep.py b/envergo/moulinette/regulations/ep.py index 5ac0f9a3e..c90e84f65 100644 --- a/envergo/moulinette/regulations/ep.py +++ b/envergo/moulinette/regulations/ep.py @@ -45,8 +45,8 @@ class EspecesProtegeesAisne(CriterionEvaluator): RESULT_MATRIX = { "interdit": RESULTS.interdit, - "derogation_inventaire": RESULTS.soumis, - "derogation_simplifiee": RESULTS.soumis, + "derogation_inventaire": RESULTS.derogation_inventaire, + "derogation_simplifiee": RESULTS.derogation_simplifiee, } def get_catalog_data(self): From deec9b61987aefcbee1dc744861adeb83c33f42b Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Fri, 24 Jan 2025 12:57:05 +0100 Subject: [PATCH 26/28] Add missing statuses to template tag --- envergo/moulinette/templatetags/moulinette.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/envergo/moulinette/templatetags/moulinette.py b/envergo/moulinette/templatetags/moulinette.py index d29ec9373..8a38a7b17 100644 --- a/envergo/moulinette/templatetags/moulinette.py +++ b/envergo/moulinette/templatetags/moulinette.py @@ -305,6 +305,8 @@ def get_display_result(regulation): RESULTS.non_soumis, RESULTS.non_concerne, RESULTS.non_disponible, + RESULTS.derogation_inventaire, + RESULTS.derogation_simplifiee, ] return regulation.result if regulation.result not in other_results else "autre" From 6d05941203cb30da5a12c9aa47deb824cb72bfc7 Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Mon, 27 Jan 2025 09:57:35 +0100 Subject: [PATCH 27/28] UPdate "derogation" label colors --- envergo/static/sass/project.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/envergo/static/sass/project.scss b/envergo/static/sass/project.scss index 8c20ffaff..5430cd483 100644 --- a/envergo/static/sass/project.scss +++ b/envergo/static/sass/project.scss @@ -105,6 +105,8 @@ article { &.probability-soumis, &.probability-systematique, + &.probability-derogation, + &.probability-derogation_simplifiee, &.probability-4 { background-color: #ffb7a5; } From 38fd2d2f99b6a16aba2d1d0dc684c170e05b20c0 Mon Sep 17 00:00:00 2001 From: Thibault Jouannic Date: Mon, 27 Jan 2025 10:21:06 +0100 Subject: [PATCH 28/28] Fix broken tests --- envergo/hedges/tests/test_models.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/envergo/hedges/tests/test_models.py b/envergo/hedges/tests/test_models.py index 244d4af2c..eab7e6b7f 100644 --- a/envergo/hedges/tests/test_models.py +++ b/envergo/hedges/tests/test_models.py @@ -1,6 +1,6 @@ import pytest -from envergo.hedges.models import R +from envergo.hedges.models import R, Species from envergo.hedges.tests.factories import ( HedgeDataFactory, HedgeFactory, @@ -10,6 +10,12 @@ pytestmark = pytest.mark.django_db +@pytest.fixture(autouse=True) +def cleanup(): + # Remove demo species + Species.objects.all().delete() + + def test_minimum_lengths_to_plant(): hedges = HedgeDataFactory( data=[ @@ -111,14 +117,14 @@ def test_species_are_filtered_by_hedge_type(): hedge = HedgeFactory(additionalData__typeHaie="degradee") hedges = HedgeDataFactory(hedges=[hedge]) - hedges_species = hedges.get_all_species() + hedges_species = hedges.get_hedge_species() assert s1 in hedges_species assert s2 in hedges_species assert s3 not in hedges_species hedge = HedgeFactory(additionalData__typeHaie="arbustive") hedges = HedgeDataFactory(hedges=[hedge]) - hedges_species = hedges.get_all_species() + hedges_species = hedges.get_hedge_species() assert s1 not in hedges_species assert s2 not in hedges_species assert s3 in hedges_species @@ -174,5 +180,5 @@ def test_multiple_hedges_combine_their_species(): assert set(hedge_species) == set([s3, s4]) hedges = HedgeDataFactory(hedges=[hedge1, hedge2]) - all_species = hedges.get_all_species() + all_species = hedges.get_hedge_species() assert set(all_species) == set([s2, s3, s4])