MSG_MOTOC / scripts /import_test.py
Samuel ADONE
Inscription libre, import de tableur, épinglage, signalement, jeu de test
2bc5c2a
Raw
History Blame Contribute Delete
9.72 kB
"""Détection des colonnes dans les tableurs de bénévoles.
Usage : python scripts/import_test.py
Couvre les formes réellement rencontrées dans les fichiers de festival :
en-têtes français accentués, séparateur point-virgule d'Excel FR, colonne
« NOM Prénom » fusionnée, numéros abîmés par Excel, feuille sans en-tête.
"""
from __future__ import annotations
import csv
import io
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from app import imports # noqa: E402
passed = failed = 0
def check(label: str, condition: bool, detail: str = "") -> None:
global passed, failed
if condition:
passed += 1
print(f" \033[32m✓\033[0m {label}")
else:
failed += 1
print(f" \033[31m✗\033[0m {label} {detail}")
def csv_bytes(rows: list[list[str]], delimiter: str = ";", encoding: str = "utf-8") -> bytes:
buffer = io.StringIO()
csv.writer(buffer, delimiter=delimiter, lineterminator="\n").writerows(rows)
return buffer.getvalue().encode(encoding)
def xlsx_bytes(rows: list[list], sheet_name: str = "Bénévoles") -> bytes:
from openpyxl import Workbook
workbook = Workbook()
sheet = workbook.active
sheet.title = sheet_name
for row in rows:
sheet.append(row)
buffer = io.BytesIO()
workbook.save(buffer)
return buffer.getvalue()
def parse(name: str, data: bytes):
return imports.parse(name, data)
def main() -> int:
print("\n\033[1m1. En-têtes français\033[0m")
data = csv_bytes(
[
["Prénom", "Nom", "Téléphone"],
["Jean", "Dupont", "06 12 34 56 78"],
["Amélie", "Nothomb", "0798765432"],
]
)
mapping, contacts, rejected = parse("benevoles.csv", data)
check("en-tête accentué reconnu", mapping.header_row == 0, str(mapping))
check("colonnes prénom/nom/téléphone", (mapping.first, mapping.last, mapping.phone) == (0, 1, 2))
check("deux contacts extraits", len(contacts) == 2, str(contacts))
check(
"premier contact correct",
contacts[0].phone == "0612345678" and contacts[0].first_name == "Jean",
str(contacts[0]),
)
check("aucun rejet", not rejected, str(rejected))
# « Prénom » contient « nom » : le piège classique de la détection.
data = csv_bytes([["Nom", "Prénom", "Portable"], ["DUPONT", "Jean", "0612345678"]])
mapping, contacts, _ = parse("x.csv", data)
check(
"ordre Nom/Prénom respecté",
(mapping.last, mapping.first) == (0, 1),
f"first={mapping.first} last={mapping.last}",
)
check(
"prénom et nom au bon endroit",
contacts[0].first_name == "Jean" and contacts[0].last_name == "DUPONT",
str(contacts[0]),
)
for header in ("Tél. portable", "N° de téléphone", "Mobile", "TELEPHONE", "Numéro"):
data = csv_bytes([["Prénom", "Nom", header], ["Jean", "Dupont", "0612345678"]])
mapping, contacts, _ = parse("x.csv", data)
check(f"variante d'intitulé « {header} »", mapping.phone == 2 and len(contacts) == 1)
print("\n\033[1m2. Colonne « Nom complet »\033[0m")
data = csv_bytes(
[
["Nom complet", "Téléphone"],
["DUPONT Jean", "0612345678"],
["Amélie Nothomb", "0798765432"],
["Martin, Claire", "0611223344"],
]
)
mapping, contacts, _ = parse("x.csv", data)
check("colonne fusionnée détectée", mapping.full == 0, str(mapping))
check(
"« DUPONT Jean » -> Jean / DUPONT",
(contacts[0].first_name, contacts[0].last_name) == ("Jean", "DUPONT"),
str(contacts[0]),
)
check(
"« Amélie Nothomb » -> Amélie / Nothomb",
(contacts[1].first_name, contacts[1].last_name) == ("Amélie", "Nothomb"),
str(contacts[1]),
)
check(
"« Martin, Claire » -> Claire / Martin",
(contacts[2].first_name, contacts[2].last_name) == ("Claire", "Martin"),
str(contacts[2]),
)
check(
"nom à particule en capitales",
imports.split_full_name("DE LA TOUR Jean-Pierre") == ("Jean-Pierre", "DE LA TOUR"),
str(imports.split_full_name("DE LA TOUR Jean-Pierre")),
)
print("\n\033[1m3. Numéros abîmés par Excel\033[0m")
data = csv_bytes(
[
["Prénom", "Nom", "Téléphone"],
["Jean", "Dupont", "612345678.0"], # zéro initial mangé, format nombre
["Luc", "Martin", "+33 6 11 22 33 44"],
["Eva", "Bernard", "0033798765432"],
["Zoé", "Petit", "06.12.34.56.79"],
]
)
_, contacts, rejected = parse("x.csv", data)
numbers = [c.phone for c in contacts]
check("zéro initial restauré", "0612345678" in numbers, str(numbers))
check("format +33 normalisé", "0611223344" in numbers, str(numbers))
check("format 0033 normalisé", "0798765432" in numbers, str(numbers))
check("séparateurs points acceptés", "0612345679" in numbers, str(numbers))
check("aucun rejet sur ces formats", not rejected, str(rejected))
print("\n\033[1m4. Lignes inexploitables\033[0m")
data = csv_bytes(
[
["Prénom", "Nom", "Téléphone"],
["Jean", "Dupont", "0612345678"],
["Sans", "Numéro", ""],
["Fixe", "Interdit", "0145678901"],
["Doublon", "Dupont", "06 12 34 56 78"],
["", "", ""],
]
)
_, contacts, rejected = parse("x.csv", data)
check("un seul contact retenu", len(contacts) == 1, str(contacts))
reasons = {r["reason"] for r in rejected}
check("ligne sans numéro rejetée", "numéro introuvable" in reasons, str(rejected))
check("doublon détecté", "doublon dans le fichier" in reasons, str(rejected))
check("ligne vide ignorée sans rejet", len(rejected) == 3, str(rejected))
print("\n\033[1m5. Sans en-tête\033[0m")
data = csv_bytes(
[
["DUPONT", "Jean", "0612345678"],
["MARTIN", "Luc", "0611223344"],
["BERNARD", "Eva", "0798765432"],
]
)
mapping, contacts, _ = parse("x.csv", data)
check("colonne téléphone trouvée par le contenu", mapping.phone == 2, str(mapping))
check("capitales identifiées comme nom de famille", mapping.last == 0 and mapping.first == 1, str(mapping))
check("pas d'hypothèse signalée", mapping.assumed_order is False)
check("trois contacts", len(contacts) == 3 and contacts[0].last_name == "DUPONT")
data = csv_bytes(
[
["Jean", "Dupont", "0612345678"],
["Luc", "Martin", "0611223344"],
]
)
mapping, _, _ = parse("x.csv", data)
check("ordre supposé signalé quand rien ne tranche", mapping.assumed_order is True, str(mapping))
print("\n\033[1m6. Encodages et séparateurs\033[0m")
data = csv_bytes([["Prénom", "Nom", "Téléphone"], ["Amélie", "Côté", "0612345678"]], encoding="cp1252")
_, contacts, _ = parse("x.csv", data)
check("CSV Windows-1252 décodé", contacts[0].last_name == "Côté", str(contacts))
data = "Prénom,Nom,Téléphone\nJean,Dupont,0612345678\n".encode("utf-8")
mapping, contacts, _ = parse("x.csv", data)
check("BOM UTF-8 et virgules", mapping.phone == 2 and len(contacts) == 1, str(mapping))
data = csv_bytes([["Prénom", "Nom", "Téléphone"], ["Jean", "Dupont", "0612345678"]], delimiter="\t")
mapping, contacts, _ = parse("x.tsv", data)
check("tabulations", mapping.phone == 2 and len(contacts) == 1, str(mapping))
print("\n\033[1m7. Fichiers .xlsx\033[0m")
data = xlsx_bytes(
[
["Prénom", "Nom", "Téléphone portable"],
["Jean", "Dupont", "0612345678"],
["Luc", "Martin", 611223344], # saisi en nombre : zéro initial perdu
]
)
mapping, contacts, rejected = parse("benevoles.xlsx", data)
check("xlsx lu", len(contacts) == 2, f"{contacts} / {rejected}")
check("numéro stocké en nombre récupéré", contacts[1].phone == "0611223344", str(contacts[1]))
check("en-tête xlsx détecté", mapping.header_row == 0 and mapping.phone == 2, str(mapping))
# Un .xlsx renommé en .csv reste identifiable par sa signature zip.
mapping, contacts, _ = parse("mal_nomme.csv", data)
check("xlsx renommé .csv reconnu", len(contacts) == 2, str(contacts))
# Lignes de titre au-dessus du tableau : cas très fréquent.
data = xlsx_bytes(
[
["Liste des bénévoles 2026"],
[],
["Prénom", "Nom", "Téléphone"],
["Jean", "Dupont", "0612345678"],
]
)
mapping, contacts, _ = parse("x.xlsx", data)
check("en-tête précédé d'un titre", mapping.header_row == 1 and len(contacts) == 1, str(mapping))
print("\n\033[1m8. Refus explicites\033[0m")
for name, blob, label in [
("vide.csv", b"", "fichier vide"),
("vieux.xls", b"\xd0\xcf\x11\xe0", "format .xls"),
]:
try:
parse(name, blob)
check(f"{label} refusé", False, "aucune erreur levée")
except imports.ImportError_ as exc:
check(f"{label} refusé", True, str(exc))
data = csv_bytes([["Prénom", "Nom", "Ville"], ["Jean", "Dupont", "Carhaix"]])
try:
parse("x.csv", data)
check("fichier sans téléphone refusé", False, "aucune erreur levée")
except imports.ImportError_:
check("fichier sans téléphone refusé", True)
print(f"\n\033[1m{passed} réussis, {failed} échoués\033[0m\n")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())