fix: A25 + A21 (partiel) — plus d'exception muque, transaction protégée (v7.3.8)
- A25 — 84 `except Exception: pass/…` → `logger.exception("<fonction>")`
(19 fichiers : api_v2 30, dashboard 10, board 7, sites 5, workspace 5,
api_v2_helpers 5, …) ; `logger` ajouté là où il manquait (api_v2_helpers,
sites + `import logging`)
- A25 critique — les `try` autour de `materialize_properties` supprimés dans
`create_collection_v2` ET `apply_db_template_v2` : un échec interrompt la
transaction au lieu de commiter une collection sans schéma
- test `test_collection_rollback_when_materialize_fails` (Bearer v2, monkeypatch
qui lève, assertions : RuntimeError + 0 ligne commitée)
- A21 partiel — `PRAGMA busy_timeout=5000` dans `get_conn()` (point d'entrée
unique) ; commentaire `ponytail:` : le wrapper async + les 510 call sites
restent à migrer module par module
- suite **1028/1028** · `ruff check app tests` OK
This commit is contained in:
+37
-42
@@ -165,7 +165,7 @@ async def get_me(request: Request, authorization: str | None = Header(default=No
|
||||
try:
|
||||
d[k] = json.loads(d[k] or "{}")
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("get_me")
|
||||
# never expose secrets
|
||||
return d
|
||||
|
||||
@@ -261,7 +261,7 @@ async def create_workspace(request: Request, authorization: str | None = Header(
|
||||
try:
|
||||
conn.execute("INSERT OR IGNORE INTO workspace_members (workspace_id, user_id, role) VALUES (?, ?, 'owner')", (wid, user["id"]))
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("create_workspace")
|
||||
conn.commit()
|
||||
row = conn.execute("SELECT * FROM workspaces WHERE id=?", (wid,)).fetchone()
|
||||
audit_log(user, "workspace.create", "workspace", wid, name, request)
|
||||
@@ -497,17 +497,13 @@ async def create_collection_v2(request: Request, authorization: str | None = Hea
|
||||
with get_conn() as conn:
|
||||
cur = conn.execute("INSERT INTO collections (name, description, icon, schema_json, workspace_id, created_by) VALUES (?, ?, ?, ?, ?, ?)", (name, description, icon, schema_json, workspace_id, user["id"]))
|
||||
cid = cur.lastrowid
|
||||
# materialize properties if schema provided
|
||||
try:
|
||||
from app.services.db_templates import materialize_properties
|
||||
materialize_properties(conn, cid, schema)
|
||||
except Exception:
|
||||
pass
|
||||
# materialize properties if schema provided — A25 : PAS de try ici,
|
||||
# une exception doit interrompre la transaction (sinon la collection est
|
||||
# commitée sans son schéma et l'erreur disparaît).
|
||||
from app.services.db_templates import materialize_properties
|
||||
materialize_properties(conn, cid, schema)
|
||||
# default view
|
||||
try:
|
||||
conn.execute("INSERT INTO collection_views (collection_id, name, view_type, config_json) VALUES (?, ?, ?, ?)", (cid, "Default View", "table", json.dumps({"visible_properties": ["Title"]})))
|
||||
except Exception:
|
||||
pass
|
||||
conn.execute("INSERT INTO collection_views (collection_id, name, view_type, config_json) VALUES (?,?,?,?)", (cid, "Default View", "table", json.dumps({"visible_properties": ["Title"]})))
|
||||
conn.commit()
|
||||
row = conn.execute("SELECT * FROM collections WHERE id=?", (cid,)).fetchone()
|
||||
audit_log(user, "collection.create", "collection", cid, name, request)
|
||||
@@ -593,20 +589,20 @@ async def create_linked_db(collection_id: int, request: Request, authorization:
|
||||
try:
|
||||
conn.execute("INSERT INTO collection_data_sources (collection_id, source_collection_id, is_linked) VALUES (?, ?, 1)", (nid, collection_id))
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("create_linked_db")
|
||||
# copy views + properties (light)
|
||||
rows = conn.execute("SELECT * FROM collection_properties WHERE collection_id=?", (collection_id,)).fetchall()
|
||||
for p in rows:
|
||||
try:
|
||||
conn.execute("INSERT INTO collection_properties (collection_id, name, prop_type, options_json, position) VALUES (?, ?, ?, ?, ?)", (nid, p["name"], p["prop_type"], p["options_json"], p["position"]))
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("create_linked_db")
|
||||
vrows = conn.execute("SELECT * FROM collection_views WHERE collection_id=?", (collection_id,)).fetchall()
|
||||
for v in vrows:
|
||||
try:
|
||||
conn.execute("INSERT INTO collection_views (collection_id, name, view_type, config_json, position) VALUES (?, ?, ?, ?, ?)", (nid, v["name"], v["view_type"], v["config_json"], v["position"]))
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("create_linked_db")
|
||||
conn.commit()
|
||||
audit_log(user, "collection.linked", "collection", nid, f"src={collection_id}", request)
|
||||
return {"id": nid, "name": name, "status": "created"}
|
||||
@@ -766,7 +762,7 @@ async def create_collection_page_v2(collection_id: int, request: Request, author
|
||||
pass
|
||||
# light validation: we rely on existing validators
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("create_collection_page_v2")
|
||||
max_pos = conn.execute("SELECT COALESCE(MAX(position), -1)+1 FROM collection_pages WHERE collection_id=?", (collection_id,)).fetchone()[0]
|
||||
# apply auto props
|
||||
try:
|
||||
@@ -774,7 +770,7 @@ async def create_collection_page_v2(collection_id: int, request: Request, author
|
||||
from app.services.property_types import apply_auto_properties as _aap
|
||||
_aap(props_list, prop_vals, user, is_create=True)
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("create_collection_page_v2")
|
||||
cur = conn.execute("INSERT INTO collection_pages (collection_id, title, icon, position, parent_id, property_values_json) VALUES (?, ?, ?, ?, ?, ?)", (collection_id, title, icon, max_pos, parent_id, json.dumps(prop_vals)))
|
||||
pid = cur.lastrowid
|
||||
conn.commit()
|
||||
@@ -783,7 +779,7 @@ async def create_collection_page_v2(collection_id: int, request: Request, author
|
||||
try:
|
||||
await _fire_event("collection.page.created", {"page_id": pid, "collection_id": collection_id, "title": title})
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("create_collection_page_v2")
|
||||
data = {"id": pid, "title": title, "status": "created", "page": row_to_dict(row)}
|
||||
key = (request.headers.get("Idempotency-Key") or "").strip()
|
||||
if key:
|
||||
@@ -857,14 +853,14 @@ async def patch_page_v2(page_id: int, request: Request, authorization: str | Non
|
||||
from app.services.property_types import apply_auto_properties as _aap
|
||||
_aap(props_list, stored, user, is_create=False)
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("patch_page_v2")
|
||||
conn.execute("UPDATE collection_pages SET title=?, icon=?, position=?, parent_id=?, property_values_json=?, updated_at=CURRENT_TIMESTAMP WHERE id=?", (title, icon, pos, parent_id, json.dumps(stored), page_id))
|
||||
conn.commit()
|
||||
audit_log(user, "page.update", "collection_page", page_id, "", request)
|
||||
try:
|
||||
await _fire_event("collection.page.updated", {"page_id": page_id, "collection_id": row["collection_id"], "title": title})
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("patch_page_v2")
|
||||
return {"id": page_id, "status": "updated"}
|
||||
|
||||
@router.delete("/pages/{page_id}")
|
||||
@@ -883,7 +879,7 @@ async def delete_page_v2(page_id: int, request: Request, authorization: str | No
|
||||
try:
|
||||
await _fire_event("collection.page.deleted", {"page_id": page_id, "collection_id": row["collection_id"]})
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("delete_page_v2")
|
||||
return {"id": page_id, "status": "deleted"}
|
||||
|
||||
@router.post("/pages/{page_id}/restore")
|
||||
@@ -901,7 +897,7 @@ async def restore_page_v2(page_id: int, request: Request, authorization: str | N
|
||||
try:
|
||||
await _fire_event("page.restored", {"page_id": page_id})
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("restore_page_v2")
|
||||
return {"id": page_id, "status": "restored"}
|
||||
raise HTTPException(404, "Page not found or not deleted")
|
||||
|
||||
@@ -1109,7 +1105,7 @@ async def create_relation_v2(prop_id: int, request: Request, authorization: str
|
||||
try:
|
||||
conn.execute("INSERT INTO collection_properties (collection_id, name, prop_type, related_collection_id, reverse_name, position) VALUES (?, ?, 'relation', ?, ?, ?)", (related_id, reverse, row["collection_id"], "", max_pos))
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("create_relation_v2")
|
||||
conn.commit()
|
||||
return {"id": prop_id, "status": "updated"}
|
||||
|
||||
@@ -1184,7 +1180,7 @@ async def create_view_v2(collection_id: int, request: Request, authorization: st
|
||||
try:
|
||||
await _fire_event("collection.view.created", {"view_id": vid, "collection_id": collection_id, "name": name, "view_type": vtype})
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("create_view_v2")
|
||||
return {"id": vid, "name": name, "view_type": vtype, "status": "created"}
|
||||
|
||||
@router.patch("/views/{view_id}")
|
||||
@@ -1208,7 +1204,7 @@ async def patch_view_v2(view_id: int, request: Request, authorization: str | Non
|
||||
try:
|
||||
cfg.update(json.loads(body["config_json"]) if isinstance(body["config_json"], str) else body["config_json"])
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("patch_view_v2")
|
||||
# also flat keys
|
||||
for k in ("group_by", "sub_group_by", "wip_limits", "card_size", "cover_property", "cover_mode", "card_properties", "visible_properties", "filters", "sorts", "date_property"):
|
||||
if k in body:
|
||||
@@ -1350,7 +1346,7 @@ async def create_comment_v2(page_id: int, request: Request, authorization: str |
|
||||
try:
|
||||
await _fire_event("comment.added", {"comment_id": nid, "page_id": page_id, "user_id": user["id"]})
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("create_comment_v2")
|
||||
return {"id": nid, "status": "created", "comment": row_to_dict(row)}
|
||||
|
||||
@router.patch("/comments/{comment_id}")
|
||||
@@ -1378,7 +1374,7 @@ async def patch_comment_v2(comment_id: int, request: Request, authorization: str
|
||||
try:
|
||||
await _fire_event("comment.resolved", {"comment_id": comment_id, "page_id": row["page_id"]})
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("patch_comment_v2")
|
||||
return {"id": comment_id, "status": "updated"}
|
||||
|
||||
@router.delete("/comments/{comment_id}")
|
||||
@@ -1419,13 +1415,13 @@ async def create_mention_v2(page_id: int, request: Request, authorization: str |
|
||||
conn.execute("INSERT INTO notifications (user_id, actor_id, ntype, title, message, resource_type, resource_id, url) VALUES (?, ?, 'mention', 'You were mentioned', ?, 'page', ?, ?)", (uid, user["id"], body.get("message") or f"Mentioned in page {page_id}", page_id, f"/pages/{page_id}"))
|
||||
created += 1
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("create_mention_v2")
|
||||
conn.commit()
|
||||
if created:
|
||||
try:
|
||||
await _fire_event("mention.added", {"page_id": page_id, "user_ids": [u for u in targets if isinstance(u, int)], "count": created})
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("create_mention_v2")
|
||||
return {"mentions": created, "status": "created"}
|
||||
|
||||
# ── Notifications ─────────────────────────────────────────────────────────
|
||||
@@ -1522,7 +1518,7 @@ async def add_favorite_v2(request: Request, authorization: str | None = Header(d
|
||||
try:
|
||||
await _fire_event("favorite.added", {"page_id": pid, "user_id": user["id"]})
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("add_favorite_v2")
|
||||
return {"page_id": pid, "status": "added"}
|
||||
|
||||
@router.delete("/favorites/{page_id}")
|
||||
@@ -1537,7 +1533,7 @@ async def remove_favorite_v2(page_id: int, request: Request, authorization: str
|
||||
try:
|
||||
await _fire_event("favorite.removed", {"page_id": page_id, "user_id": user["id"]})
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("remove_favorite_v2")
|
||||
return {"page_id": page_id, "status": "removed"}
|
||||
|
||||
@router.get("/tags")
|
||||
@@ -1687,7 +1683,7 @@ async def create_share_v2(page_id: int, request: Request, authorization: str | N
|
||||
try:
|
||||
await _fire_event("page.shared", {"page_id": page_id, "share_id": nid, "permission": perm})
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("create_share_v2")
|
||||
return {"id": nid, "page_id": page_id, "status": "shared"}
|
||||
|
||||
@router.patch("/shares/{share_id}")
|
||||
@@ -1748,7 +1744,7 @@ async def publish_page_v2(page_id: int, request: Request, authorization: str | N
|
||||
try:
|
||||
await _fire_event("page.published", {"page_id": page_id, "slug": slug})
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("publish_page_v2")
|
||||
return {"page_id": page_id, "slug": slug, "url": f"/p/{slug}", "status": "published"}
|
||||
|
||||
@router.delete("/pages/{page_id}/publish")
|
||||
@@ -1763,7 +1759,7 @@ async def unpublish_page_v2(page_id: int, request: Request, authorization: str |
|
||||
try:
|
||||
await _fire_event("page.unpublished", {"page_id": page_id})
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("unpublish_page_v2")
|
||||
return {"page_id": page_id, "status": "unpublished"}
|
||||
|
||||
# ── History ───────────────────────────────────────────────────────────────
|
||||
@@ -1844,7 +1840,7 @@ async def create_sprint_v2(collection_id: int, request: Request, authorization:
|
||||
try:
|
||||
await _fire_event("sprint.created", {"sprint_id": sid, "collection_id": collection_id, "name": name})
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("create_sprint_v2")
|
||||
return {"id": sid, "name": name, "status": "created"}
|
||||
|
||||
@router.patch("/sprints/{sprint_id}")
|
||||
@@ -1871,7 +1867,7 @@ async def patch_sprint_v2(sprint_id: int, request: Request, authorization: str |
|
||||
try:
|
||||
await _fire_event("sprint.updated", {"sprint_id": sprint_id, "collection_id": row["collection_id"], "name": name, "status": status})
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("patch_sprint_v2")
|
||||
return {"id": sprint_id, "status": "updated"}
|
||||
|
||||
@router.delete("/sprints/{sprint_id}")
|
||||
@@ -1937,7 +1933,7 @@ async def burndown_v2(sprint_id: int, request: Request, authorization: str | Non
|
||||
completed += 1
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
logger.exception("burndown_v2")
|
||||
remaining = total - completed
|
||||
# ideal linear
|
||||
ideal = [round(total * (1 - i / 10)) for i in range(11)]
|
||||
@@ -2047,11 +2043,10 @@ async def apply_db_template_v2(template_id: int, request: Request, authorization
|
||||
schema = json.loads(tpl["schema_json"] or "[]")
|
||||
cur = conn.execute("INSERT INTO collections (name, description, icon, schema_json, workspace_id, created_by) VALUES (?, ?, ?, ?, ?, ?)", (name, tpl["description"], tpl["icon"] if "icon" in tpl.keys() else "📋", json.dumps(schema), body.get("workspace_id"), user["id"]))
|
||||
cid = cur.lastrowid
|
||||
try:
|
||||
from app.services.db_templates import materialize_properties
|
||||
materialize_properties(conn, cid, schema)
|
||||
except Exception:
|
||||
pass
|
||||
# A25 : pas de try — un échec de matérialisation doit interrompre la
|
||||
# transaction plutôt que de commiter une collection sans schéma.
|
||||
from app.services.db_templates import materialize_properties
|
||||
materialize_properties(conn, cid, schema)
|
||||
conn.commit()
|
||||
return {"collection_id": cid, "name": name, "status": "created"}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user