Skip to content

Commit

Permalink
Amélioration de l'extraction de données
Browse files Browse the repository at this point in the history
- Ajout de nouveaux champs dans l'export,
- Ajout des headers dans tuple pour éviter d'être dépendant du verbose_name et éviter migrations, etc...
- Gestion des champs imbriqués (cf. `get_field_value` et champs dans les listes (exemple: champ1__champ2)
- La méthode export a été modifiée pour améliorer le format CSV :
  - `csv.QUOTE_ALL` : met des guillemets autour de chaque valeur, évitant les erreurs avec les virgules et caractères spéciaux.
  - `doublequote=True` : double les guillemets dans le texte. Exemple :
    Avant : "Un "mot" important" → incorrect
    Après : "Un ""mot"" important" → correct

Tests:
- Création des factories `StatutEvenementFactory`, `ContexteFactory`, et `SiteInspectionFactory`
- Mise à jour des factories existantes avec les nouveaux champs et relations `statut_evenement`, `contexte`, et `site_inspection`.
- Ajustement du nombre de requêtes attendues dans les tests de performance pour prendre en compte les nouveaux champs et relations dans les factories
  • Loading branch information
alanzirek committed Feb 1, 2025
1 parent 1e70ccf commit dcfc1ec
Show file tree
Hide file tree
Showing 4 changed files with 385 additions and 173 deletions.
200 changes: 139 additions & 61 deletions sv/export.py
Original file line number Diff line number Diff line change
@@ -1,95 +1,173 @@
from .models import FicheDetection, Prelevement, Lieu, StructurePreleveuse
from functools import reduce

from .models import FicheDetection
import csv


class FicheDetectionExport:
fields = [
"numero",
"numero_europhyt",
"numero_rasff",
"statut_evenement",
"date_premier_signalement",
"commentaire",
"mesures_conservatoires_immediates",
"mesures_consignation",
"mesures_phytosanitaires",
"mesures_surveillance_specifique",
"date_creation",
fiche_detection_fields = [
("numero", "Numéro de fiche"),
("evenement", "Num. événement"),
("evenement__organisme_nuisible__libelle_court", "Organisme nuisible"),
("evenement__organisme_nuisible__code_oepp", "Code OEPP"),
("evenement__statut_reglementaire__libelle", "Statut réglementaire"),
("date_creation", "Date de création"),
("createur", "Structure créatrice"),
("numero_europhyt", "Numéro Europhyt"),
("numero_rasff", "Numéro RASFF"),
("statut_evenement", "Statut de l'événement"),
("contexte", "Contexte"),
("vegetaux_infestes", "Nombre ou volume de végétaux infestés"),
("date_premier_signalement", "Date premier signalement"),
("commentaire", "Commentaire"),
("mesures_conservatoires_immediates", "Mesures conservatoires immédiates"),
("mesures_consignation", "Mesures de consignation"),
("mesures_phytosanitaires", "Mesures phytosanitaires"),
("mesures_surveillance_specifique", "Mesures de surveillance spécifique"),
]
lieux_fields = [
"nom",
"wgs84_longitude",
"wgs84_latitude",
"adresse_lieu_dit",
"commune",
"code_insee",
"departement",
("nom", "Nom"),
("adresse_lieu_dit", "Adresse ou lieu-dit"),
("commune", "Commune"),
("site_inspection", "Site d'inspection"),
("wgs84_longitude", "Longitude WGS84"),
("wgs84_latitude", "Latitude WGS84"),
("nom_etablissement", "Nom établissement"),
("activite_etablissement", "Activité établissement"),
("pays_etablissement", "Pays établissement"),
("raison_sociale_etablissement", "Raison sociale établissement"),
("adresse_etablissement", "Adresse établissement"),
("siret_etablissement", "SIRET établissement"),
("code_inupp_etablissement", "Code INUPP"),
]
prelevement_fields = [
"numero_echantillon",
"date_prelevement",
"is_officiel",
"resultat",
"structure_preleveuse",
"matrice_prelevee",
"espece_echantillon",
"laboratoire",
("type_analyse", "Type d'analyse"),
("is_officiel", "Prélèvement officiel"),
("numero_rapport_inspection", "Numéro du rapport d'inspection"),
("laboratoire", "Laboratoire"),
("numero_echantillon", "N° d'échantillon"),
("structure_preleveuse", "Structure préleveuse"),
("date_prelevement", "Date de prélèvement"),
("matrice_prelevee", "Nature de l'objet"),
("espece_echantillon", "Espèce de l'échantillon"),
("resultat", "Résultat"),
]
fiche_zone_delimitee_fields = [
("evenement__fiche_zone_delimitee__commentaire", "Commentaire zone délimitée"),
("evenement__fiche_zone_delimitee__rayon_zone_tampon", "Rayon tampon réglementaire ou arbitré"),
("evenement__fiche_zone_delimitee__surface_tampon_totale", "Surface tampon totale"),
]
zone_infestee_fields = [
("zone_infestee__nom", "Nom de la zone infestée"),
("zone_infestee__caracteristique_principale", "Caractéristique principale"),
("zone_infestee__rayon", "Rayon de la zone infestée"),
("zone_infestee__surface_infestee_totale", "Surface infestée totale"),
]

def _clean_field_name(self, field, instance):
return instance._meta.get_field(field).verbose_name

def get_queryset(self, user):
queryset = FicheDetection.objects.all().get_fiches_user_can_view(user).optimized_for_details()
queryset = queryset.prefetch_related(
"lieux",
"lieux__prelevements",
"lieux__departement",
"lieux__prelevements__structure_preleveuse",
"lieux__prelevements__espece_echantillon",
"lieux__prelevements__matrice_prelevee",
"lieux__prelevements__laboratoire",
return (
FicheDetection.objects.all()
.get_fiches_user_can_view(user)
.optimized_for_details()
.select_related(
"evenement__numero",
"evenement__organisme_nuisible",
"evenement__statut_reglementaire",
)
.prefetch_related(
"lieux",
"lieux__departement",
"lieux__site_inspection",
"lieux__prelevements",
"lieux__prelevements__structure_preleveuse",
"lieux__prelevements__espece_echantillon",
"lieux__prelevements__matrice_prelevee",
"lieux__prelevements__laboratoire",
)
)
return queryset

def get_fieldnames(self):
empty_prelevement = Prelevement(lieu=Lieu(), structure_preleveuse=StructurePreleveuse())
return self.get_fiche_data_with_prelevement(FicheDetection(), empty_prelevement).keys()
"""Retourne les noms des champs pour l'en-tête du CSV"""
all_fields = (
self.fiche_detection_fields
+ self.lieux_fields
+ self.prelevement_fields
+ self.fiche_zone_delimitee_fields
+ self.zone_infestee_fields
)
return [header for _, header in all_fields]

def get_field_value(self, instance, field):
"""Récupère et formate la valeur d'un champ, en suivant les relations Django si nécessaire."""
return reduce(lambda obj, attr: getattr(obj, attr, None) if obj else None, field.split("__"), instance)

def add_data(self, result, instance, fields):
for field, header in fields:
result[header] = self.get_field_value(instance, field)
return result

def add_fiche_detection_data(self, result, fiche):
return self.add_data(result, fiche, self.fiche_detection_fields)

def add_lieu_data(self, result, lieu):
return self.add_data(result, lieu, self.lieux_fields)

def add_prelevement_data(self, result, prelevement):
return self.add_data(result, prelevement, self.prelevement_fields)

def add_zone_delimitee_data(self, result, fiche):
return self.add_data(result, fiche, self.fiche_zone_delimitee_fields)

def add_zone_infestee_data(self, result, fiche):
return self.add_data(result, fiche, self.zone_infestee_fields)

def get_fiche_data(self, fiche):
result = {}
for field in self.fields:
result[self._clean_field_name(field, fiche)] = getattr(fiche, field)
self.add_fiche_detection_data(result, fiche)
self.add_zone_delimitee_data(result, fiche)
self.add_zone_infestee_data(result, fiche)
return result

def get_fiche_data_with_lieu(self, fiche, lieu):
result = self.get_fiche_data(fiche)
for field in self.lieux_fields:
result[self._clean_field_name(field, lieu)] = getattr(lieu, field)
result = {}
self.add_fiche_detection_data(result, fiche)
self.add_lieu_data(result, lieu)
self.add_zone_delimitee_data(result, fiche)
self.add_zone_infestee_data(result, fiche)
return result

def get_fiche_data_with_prelevement(self, fiche, prelevement):
result = self.get_fiche_data_with_lieu(fiche, prelevement.lieu)
for field in self.prelevement_fields:
result[self._clean_field_name(field, prelevement)] = getattr(prelevement, field)
result = {}
self.add_fiche_detection_data(result, fiche)
self.add_lieu_data(result, prelevement.lieu)
self.add_prelevement_data(result, prelevement)
self.add_zone_delimitee_data(result, fiche)
self.add_zone_infestee_data(result, fiche)
return result

def get_lines_from_instance(self, fiche_detection):
lieux = fiche_detection.lieux.all()
if lieux:
for lieu in lieux:
prelevements = lieu.prelevements.all()
if prelevements:
for prelevement in prelevements:
yield self.get_fiche_data_with_prelevement(fiche_detection, prelevement)
else:
yield self.get_fiche_data_with_lieu(fiche_detection, lieu)
else:
if not lieux:
yield self.get_fiche_data(fiche_detection)
return

for lieu in lieux:
prelevements = lieu.prelevements.all()
if not prelevements:
yield self.get_fiche_data_with_lieu(fiche_detection, lieu)
continue

for prelevement in prelevements:
yield self.get_fiche_data_with_prelevement(fiche_detection, prelevement)

def export(self, stream, user):
queryset = self.get_queryset(user)
writer = csv.DictWriter(stream, fieldnames=self.get_fieldnames())
writer = csv.DictWriter(
stream,
fieldnames=self.get_fieldnames(),
quoting=csv.QUOTE_ALL, # Force les guillemets sur tous les champs
doublequote=True, # Double les guillemets dans le contenu
)
writer.writeheader()

for instance in queryset:
Expand Down
42 changes: 42 additions & 0 deletions sv/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
MatricePrelevee,
EspeceEchantillon,
Laboratoire,
StatutEvenement,
Contexte,
SiteInspection,
)
from datetime import datetime

Expand Down Expand Up @@ -107,6 +110,30 @@ class Meta:
confirmation_officielle = False


class StatutEvenementFactory(DjangoModelFactory):
class Meta:
model = StatutEvenement
django_get_or_create = ("libelle",)

libelle = factory.Sequence(lambda n: f"Statut évènement {n}")


class ContexteFactory(DjangoModelFactory):
class Meta:
model = Contexte
django_get_or_create = ("nom",)

nom = factory.Sequence(lambda n: f"Contexte {n}")


class SiteInspectionFactory(DjangoModelFactory):
class Meta:
model = SiteInspection
django_get_or_create = ("nom",)

nom = factory.Sequence(lambda n: f"Site d'inspection {n}")


class PrelevementFactory(DjangoModelFactory):
class Meta:
model = Prelevement
Expand Down Expand Up @@ -159,6 +186,7 @@ class Meta:
adresse_etablissement = factory.Faker("address")
siret_etablissement = factory.Faker("numerify", text="##############")
code_inupp_etablissement = factory.Faker("numerify", text="#######")
site_inspection = factory.SubFactory("sv.factories.SiteInspectionFactory")


class FicheDetectionFactory(DjangoModelFactory):
Expand All @@ -177,6 +205,8 @@ class Meta:
vegetaux_infestes = factory.Faker("sentence")
numero = factory.SubFactory("sv.factories.NumeroFicheFactory")
evenement = factory.SubFactory("sv.factories.EvenementFactory")
statut_evenement = factory.SubFactory("sv.factories.StatutEvenementFactory")
contexte = factory.SubFactory("sv.factories.ContexteFactory")

@factory.lazy_attribute
def createur(self):
Expand Down Expand Up @@ -213,6 +243,12 @@ def createur(self):
def from_detection(cls, detection: FicheDetection, **kwargs):
return cls(evenement=detection.evenement, **kwargs)

@factory.post_generation
def hors_zone_infestee(self, create, extracted, **kwargs):
if extracted:
self.hors_zone_infestee = extracted
self.save()


class ZoneInfesteeFactory(DjangoModelFactory):
class Meta:
Expand Down Expand Up @@ -251,3 +287,9 @@ def date_creation(self, create, extracted, **kwargs): # noqa: F811
else:
self.date_creation = extracted
self.save()

@factory.post_generation
def fiche_zone_delimitee(self, create, extracted, **kwargs):
if extracted:
self.fiche_zone_delimitee = extracted
self.save()
10 changes: 5 additions & 5 deletions sv/tests/test_evenement_performance_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ def test_evenement_performances_with_lieux(client, django_assert_num_queries):
fiche_detection = FicheDetectionFactory(evenement=evenement)
client.get(evenement.get_absolute_url())

with django_assert_num_queries(BASE_NUM_QUERIES + 9):
with django_assert_num_queries(BASE_NUM_QUERIES + 11):
client.get(evenement.get_absolute_url())

baker.make(Lieu, fiche_detection=fiche_detection, _quantity=3, _fill_optional=True)
with django_assert_num_queries(BASE_NUM_QUERIES + 16):
with django_assert_num_queries(BASE_NUM_QUERIES + 18):
client.get(evenement.get_absolute_url())


Expand All @@ -75,12 +75,12 @@ def test_evenement_performances_with_prelevement(client, django_assert_num_queri
fiche_detection = FicheDetectionFactory(evenement=evenement)
client.get(evenement.get_absolute_url())

with django_assert_num_queries(BASE_NUM_QUERIES + 9):
with django_assert_num_queries(BASE_NUM_QUERIES + 11):
client.get(evenement.get_absolute_url())

PrelevementFactory.create_batch(3, lieu__fiche_detection=fiche_detection)

with django_assert_num_queries(BASE_NUM_QUERIES + 16):
with django_assert_num_queries(BASE_NUM_QUERIES + 19):
client.get(evenement.get_absolute_url())


Expand Down Expand Up @@ -123,5 +123,5 @@ def test_fiche_zone_delimitee_with_multiple_zone_infestee(

client.get(evenement.get_absolute_url())

with django_assert_num_queries(BASE_NUM_QUERIES + 38):
with django_assert_num_queries(BASE_NUM_QUERIES + 44):
client.get(evenement.get_absolute_url())
Loading

0 comments on commit dcfc1ec

Please sign in to comment.