| """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 |
|
|
| 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)) |
|
|
| |
| 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"], |
| ["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], |
| ] |
| ) |
| 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)) |
|
|
| |
| mapping, contacts, _ = parse("mal_nomme.csv", data) |
| check("xlsx renommé .csv reconnu", len(contacts) == 2, str(contacts)) |
|
|
| |
| 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()) |
|
|