feat: formats tableur additionnels - xlsm editable, xls/ods lecture seule, csv editable #153

A16 — la visionneuse tableur accepte quatre formats de plus : .xlsm est
servi et sauvegarde comme un .xlsx avec keep_vba=True (les macros
survivent, la porte lossy est levée pour ce format) ; .xls (xlrd) et .ods
(odfpy) sont rendus en lecture seule (xlsx_readonly, wiring d'édition
désactivé) ; .csv devient éditable via render_csv_table (grille A1
identique au viewer) et PUT /api/file/{vault}/csv/save (réécriture csv
RFC 4180, extension de grille, valeurs stockées telles quelles). 12 tests
backend + contre-preuve (4 échecs sur neutralisation du service CSV),
JSDOM 33/33, ruff/mypy 0.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
This commit is contained in:
2026-09-28 16:27:10 -04:00
parent dff32a97ee
commit ca6407e0c0
18 changed files with 624 additions and 52 deletions
+38
View File
@@ -73,6 +73,9 @@ from backend.services.mutations import (
from backend.services.mutations import (
rename_file as service_rename_file,
)
from backend.services.mutations import (
save_csv_cells as service_save_csv_cells,
)
from backend.share import update_shares_after_rename
from backend.sse import sse_manager
from backend.webhooks import dispatch_webhooks
@@ -177,6 +180,41 @@ def api_file_xlsx_save(
return {"status": "ok", "vault": result["vault"], "path": result["path"], "size": result["size"]}
@router.put("/api/file/{vault_name}/csv/save", response_model=FileSaveResponse)
def api_file_csv_save(
vault_name: str,
path: str = Query(..., description="Relative path to the .csv file"),
body: dict = Body(
...,
description='{"cells": {"A1": value}} — A1-addressed text edits (#153 A16)',
),
current_user=Depends(require_auth),
):
"""Apply A1-addressed cell edits to a ``.csv`` file (#153 A16).
The grid is re-parsed with :mod:`csv`, patched and re-serialized
(RFC 4180 quoting). References beyond the extent grow the grid. Values
are stored verbatim as text — a CSV has no formula engine.
"""
if not check_vault_access(vault_name, current_user):
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
cells = body.get("cells")
if not isinstance(cells, dict) or not cells or len(cells) > 500:
raise HTTPException(status_code=400, detail="Cellules invalides (1 à 500 par requête)")
for ref, value in cells.items():
if not isinstance(ref, str) or not isinstance(value, (str, int, float, bool, type(None))):
raise HTTPException(status_code=400, detail=f"Cellule invalide: {ref!r}")
result = service_save_csv_cells(vault_name, path, cells)
log_file_save(
current_user["username"], vault_name, path,
sum(len(str(v)) for v in cells.values()),
current_user.get("_request_ip", "unknown"),
)
return {"status": "ok", "vault": result["vault"], "path": result["path"], "size": result["size"]}
@router.put("/api/file/{vault_name}/xlsx/structure", response_model=FileSaveResponse)
def api_file_xlsx_structure(
vault_name: str,