Étape 3 : import CSV/Excel des bénévoles et disponibilités
- Fichier unique, processus en deux temps : POST /editions/{id}/import/analyze
(colonnes détectées, aperçu, mapping suggéré, demi-journées de l'édition)
puis POST /editions/{id}/import/execute (mapping validé)
- Parser CSV (délimiteur et encodage devinés, BOM) et XLSX (openpyxl),
détection XLSX par nombre magique
- Vocabulaire des valeurs de disponibilité (tri-état, accents/casse) et des
tailles de t-shirt ; compétences créées à la volée dans le référentiel
- Rapport ligne par ligne : codes de traduction + paramètres, jamais de
phrase. Doublons : upsert sur email, signalement sur (nom, prénom) sans
email, doublons intra-fichier. Mode simulation (rollback, rien de persisté)
- 40 tests : parser, valeurs, service (tous les cas), endpoints
- Dépendances : python-multipart, openpyxl
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
cb2e84adf0
commit
bb919554ff
@@ -0,0 +1,63 @@
|
||||
import io
|
||||
|
||||
import pytest
|
||||
from openpyxl import Workbook
|
||||
|
||||
from app.importing.parser import ImportParseError, parse_upload
|
||||
|
||||
|
||||
def _xlsx_bytes(rows: list[list[object]]) -> bytes:
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
for row in rows:
|
||||
ws.append(row)
|
||||
buffer = io.BytesIO()
|
||||
wb.save(buffer)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def test_csv_semicolon_with_bom():
|
||||
raw = "Prénom;Nom;Email\nAlex;Martin;a@b.fr\n".encode()
|
||||
table = parse_upload("benevoles.csv", raw)
|
||||
assert table.headers == ["Prénom", "Nom", "Email"]
|
||||
assert table.as_dicts() == [
|
||||
{"Prénom": "Alex", "Nom": "Martin", "Email": "a@b.fr"}
|
||||
]
|
||||
|
||||
|
||||
def test_csv_comma_delimiter():
|
||||
raw = b"first,last\nSam,Nguyen\n"
|
||||
table = parse_upload("x.csv", raw)
|
||||
assert table.headers == ["first", "last"]
|
||||
assert table.rows == [["Sam", "Nguyen"]]
|
||||
|
||||
|
||||
def test_csv_blank_lines_dropped_and_short_rows_padded():
|
||||
raw = b"a;b;c\n\n1;2\n3;4;5;6\n"
|
||||
table = parse_upload("x.csv", raw)
|
||||
assert table.rows == [["1", "2", ""], ["3", "4", "5"]]
|
||||
|
||||
|
||||
def test_xlsx_detected_by_magic_without_extension():
|
||||
data = _xlsx_bytes([["Prénom", "Nom"], ["Alex", "Martin"], [None, None]])
|
||||
table = parse_upload("upload.dat", data)
|
||||
assert table.headers == ["Prénom", "Nom"]
|
||||
assert table.rows == [["Alex", "Martin"]]
|
||||
|
||||
|
||||
def test_xlsx_numeric_cells_become_plain_strings():
|
||||
data = _xlsx_bytes([["Nom", "Tel"], ["Martin", 601020304]])
|
||||
table = parse_upload("x.xlsx", data)
|
||||
assert table.rows == [["Martin", "601020304"]]
|
||||
|
||||
|
||||
def test_empty_file_raises():
|
||||
with pytest.raises(ImportParseError) as err:
|
||||
parse_upload("x.csv", b"")
|
||||
assert err.value.code == "import.error.empty_file"
|
||||
|
||||
|
||||
def test_unreadable_xlsx_raises():
|
||||
with pytest.raises(ImportParseError) as err:
|
||||
parse_upload("x.xlsx", b"not a real workbook")
|
||||
assert err.value.code == "import.error.unreadable_file"
|
||||
Reference in New Issue
Block a user