Add support to import with xl file format
This commit is contained in:
parent
e0169b803d
commit
d338722880
@ -1493,6 +1493,32 @@ def _parse_csv_rows(content: bytes) -> list[dict]:
|
|||||||
return [dict(row) for row in reader]
|
return [dict(row) for row in reader]
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_xlsx_rows(content: bytes) -> list[dict]:
|
||||||
|
"""Parse an XLSX (Excel) file and return a list of dicts.
|
||||||
|
Uses the first sheet; first row is treated as the header.
|
||||||
|
"""
|
||||||
|
import openpyxl
|
||||||
|
wb = openpyxl.load_workbook(io.BytesIO(content), read_only=True, data_only=True)
|
||||||
|
ws = wb.active
|
||||||
|
rows = list(ws.iter_rows(values_only=True))
|
||||||
|
wb.close()
|
||||||
|
if not rows:
|
||||||
|
return []
|
||||||
|
# First row = headers; normalise None headers to empty string
|
||||||
|
headers = [str(h).strip() if h is not None else "" for h in rows[0]]
|
||||||
|
result = []
|
||||||
|
for row in rows[1:]:
|
||||||
|
# Skip completely empty rows
|
||||||
|
if all(v is None or str(v).strip() == "" for v in row):
|
||||||
|
continue
|
||||||
|
result.append({
|
||||||
|
headers[i]: (str(v).strip() if v is not None else "")
|
||||||
|
for i, v in enumerate(row)
|
||||||
|
if i < len(headers) and headers[i] # skip header-less columns
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _parse_json_rows(content: bytes) -> list[dict]:
|
def _parse_json_rows(content: bytes) -> list[dict]:
|
||||||
"""Parse a JSON file — supports array at root OR {data: [...]}."""
|
"""Parse a JSON file — supports array at root OR {data: [...]}."""
|
||||||
payload = json.loads(content.decode("utf-8-sig", errors="replace"))
|
payload = json.loads(content.decode("utf-8-sig", errors="replace"))
|
||||||
@ -1593,16 +1619,20 @@ async def import_contacts(
|
|||||||
try:
|
try:
|
||||||
if filename.endswith(".json"):
|
if filename.endswith(".json"):
|
||||||
raw_rows = _parse_json_rows(content)
|
raw_rows = _parse_json_rows(content)
|
||||||
elif filename.endswith(".csv") or filename.endswith(".xlsx"):
|
elif filename.endswith(".xlsx"):
|
||||||
# For XLSX export from our own app, treat as CSV (xlsx export from
|
raw_rows = _parse_xlsx_rows(content)
|
||||||
# GuestList produces proper column headers in English)
|
elif filename.endswith(".csv"):
|
||||||
raw_rows = _parse_csv_rows(content)
|
raw_rows = _parse_csv_rows(content)
|
||||||
else:
|
else:
|
||||||
# Sniff: try JSON then CSV
|
# Sniff: try JSON → xlsx magic bytes → CSV
|
||||||
try:
|
try:
|
||||||
raw_rows = _parse_json_rows(content)
|
raw_rows = _parse_json_rows(content)
|
||||||
except Exception:
|
except Exception:
|
||||||
raw_rows = _parse_csv_rows(content)
|
# XLSX files start with PK (zip magic bytes 50 4B)
|
||||||
|
if content[:2] == b'PK':
|
||||||
|
raw_rows = _parse_xlsx_rows(content)
|
||||||
|
else:
|
||||||
|
raw_rows = _parse_csv_rows(content)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(status_code=422, detail=f"Cannot parse file: {exc}")
|
raise HTTPException(status_code=422, detail=f"Cannot parse file: {exc}")
|
||||||
|
|
||||||
|
|||||||
@ -6,3 +6,4 @@ pydantic[email]>=2.5.0
|
|||||||
httpx>=0.25.2
|
httpx>=0.25.2
|
||||||
python-dotenv>=1.0.0
|
python-dotenv>=1.0.0
|
||||||
python-multipart>=0.0.7
|
python-multipart>=0.0.7
|
||||||
|
openpyxl>=3.1.2
|
||||||
|
|||||||
@ -118,7 +118,7 @@ function ImportContacts({ eventId, onImportComplete }) {
|
|||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
accept=".csv,.json"
|
accept=".csv,.json,.xlsx"
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user