Files
flowdeck/app/routers/my_tasks.py
T
bruno 224bda74d5
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
fix: A21 phase 1 — 352 routes async sans await → threadpool (v7.8.0)
- Conversion `async def` → `def` de TOUTES les routes dont le corps ne contient
  ni `await`, ni `async with`, ni `async for`, ni `asyncio` (scan automatique
  corps par corps sur app/ : 352 converties, 0 dangereuses, vérifié
  `asyncio`/`run_coroutine`/`.result()` absents). FastAPI exécute ces handlers
  dans son threadpool → tout leur SQLite (`get_conn()` + `conn.execute`) quitte
  l'event loop, sans changer une ligne de logique.
- Répartition : api_v2 60, dashboard 40, collections 25, board 23,
  workspace 19, wiki 17, permissions 14, api 14, main.py 6, + 35 fichiers.
- Les 4 routers prioritaires de l'audit sont couverts par ce lot :
  api_v2 60 + dashboard 40 + collections 25 + board 23 = 148 conversions
  (le reste de leurs routes attend la phase 2 : elles ont de vrais `await`).
- Reste (phase 2) : les 311 routes avec de vrais `await` → enrouler les blocs
  DB dans `await anyio.to_thread.run_sync(...)` ; pas de wrapper partagé livré
  (rien ne l'appellerait — YAGNI jusqu'au premier usage).

suite **1037/1037** (233 s) · `ruff check app tests` OK · docs à jour
2026-10-01 10:53:26 -04:00

148 lines
6.6 KiB
Python

"""FlowDeck — My Tasks router (v1.9.0)."""
from __future__ import annotations
import json
import logging
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from app.auth.session import SessionManager
from app.db import get_conn
from app.templating import ENV
logger = logging.getLogger(__name__)
router = APIRouter(tags=["my-tasks"], prefix="/my-tasks")
def _get_current_user(request: Request) -> dict | None:
session = request.cookies.get("flowdeck_session", "")
return SessionManager.decode_session(session)
@router.get("", response_class=HTMLResponse)
def my_tasks_dashboard(request: Request, view: str = "all", days: int = 7):
"""My Tasks — aggregates all pages assigned to the current user across all collections."""
user = _get_current_user(request)
user_login = user.get("login", "admin") if user else "admin"
with get_conn() as conn:
collections = conn.execute("SELECT * FROM collections ORDER BY name").fetchall()
grouped: dict[str, list[dict]] = {}
for col in collections:
col_id = col["id"]
pages = conn.execute(
"SELECT * FROM collection_pages WHERE collection_id=? AND parent_id IS NULL ORDER BY position",
(col_id,),
).fetchall()
collection_tasks = []
for p in pages:
props = {}
try:
props = json.loads(p["property_values_json"] or "{}")
except (json.JSONDecodeError, TypeError):
pass
due_date = props.get("due_date", "")
status = props.get("status", "—")
assignee = props.get("assignee", "")
if view != "all" and assignee and assignee != user_login:
continue
collection_tasks.append({
"id": p["id"],
"title": p["title"] or "Untitled",
"icon": p["icon"] or "📋",
"status": status,
"due_date": due_date,
})
if collection_tasks:
grouped[col["name"]] = collection_tasks
total = sum(len(t) for t in grouped.values())
# Build content HTML
sections = ""
for col_name, tasks in grouped.items():
items = ""
for t in tasks:
due_badge = f"<span class='mt-due'>{t['due_date']}</span>" if t.get("due_date") else ""
status_cls = t['status'].lower().replace(' ', '-') if t.get('status') else 'none'
status_badge = f"<span class='mt-status mt-{status_cls}'>{t.get('status', '—')}</span>"
items += f"<div class='mt-item'><span class='mt-icon'>{t['icon']}</span><span class='mt-title'>{t['title']}</span>{status_badge}{due_badge}</div>"
sections += f"<div class='mt-section'><div class='mt-section-header'>{col_name} <span class='mt-count'>{len(tasks)}</span></div>{items}</div>"
content_html = f"""<div style="max-width:800px;margin:0 auto;padding:40px 24px;">
<h1 style="font-size:24px;margin:0 0 8px;">📋 My Tasks</h1>
<p style="color:var(--text-dim);font-size:14px;margin-bottom:24px;">{total} tasks across {len(grouped)} collections</p>
<div class="view-tabs" style="display:flex;gap:4px;margin-bottom:24px;border-bottom:1px solid var(--border);padding-bottom:12px;">
<a class="tab{' active' if view=='all' else ''}" href="/my-tasks?view=all" style="padding:6px 14px;border-radius:6px;cursor:pointer;color:var(--text-dim);font-size:13px;text-decoration:none;">All</a>
<a class="tab{' active' if view=='today' else ''}" href="/my-tasks?view=today" style="padding:6px 14px;border-radius:6px;cursor:pointer;color:var(--text-dim);font-size:13px;text-decoration:none;">Today</a>
<a class="tab{' active' if view=='overdue' else ''}" href="/my-tasks?view=overdue" style="padding:6px 14px;border-radius:6px;cursor:pointer;color:var(--text-dim);font-size:13px;text-decoration:none;">Overdue</a>
<a class="tab{' active' if view=='upcoming' else ''}" href="/my-tasks?view=upcoming&days=7" style="padding:6px 14px;border-radius:6px;cursor:pointer;color:var(--text-dim);font-size:13px;text-decoration:none;">Next 7 days</a>
</div>
<style>
.tab.active{{background:var(--accent);color:#fff!important;}}
.tab:hover{{background:var(--bg-hover);color:var(--text);}}
.mt-section{{margin-bottom:24px;}}
.mt-section-header{{font-size:13px;font-weight:600;color:var(--text-dim);margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px;}}
.mt-count{{color:var(--text-dim);font-weight:400;opacity:.5;}}
.mt-item{{display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--bg-card);border-radius:8px;margin-bottom:3px;border:1px solid var(--border);}}
.mt-item:hover{{border-color:var(--accent);}}
.mt-icon{{font-size:16px;}}
.mt-title{{flex:1;font-size:14px;}}
.mt-status{{font-size:11px;padding:2px 8px;border-radius:4px;}}
.mt-due{{font-size:11px;color:var(--text-dim);}}
.mt-todo,.mt-none{{background:rgba(255,255,255,.05);color:var(--text-dim);}}
.mt-in-progress{{background:rgba(35,131,226,.15);color:#2C8CEB;}}
.mt-done,.mt-completed,.mt-complete{{background:rgba(0,200,100,.15);color:#00CC66;}}
</style>
{sections}
</div>"""
from app.routers.dashboard import _sidebar_data
env = ENV
sidebar = _sidebar_data(request, [])
block_tpl = env.from_string(
'{% extends "base.html" %}{% block content %}{{ content_html|safe }}{% endblock %}'
)
return block_tpl.render(
**sidebar,
request=request,
content_html=content_html,
page_title="My Tasks",
title_prefix="My Tasks",
page_icon="📋",
)
@router.get("/api")
def my_tasks_api(request: Request, view: str = "all", days: int = 7):
"""API: return my tasks as JSON."""
user = _get_current_user(request)
user.get("login", "admin") if user else "admin"
with get_conn() as conn:
collections = conn.execute("SELECT * FROM collections ORDER BY name").fetchall()
result = {}
for col in collections:
pages = conn.execute(
"SELECT * FROM collection_pages WHERE collection_id=? AND parent_id IS NULL ORDER BY position",
(col["id"],),
).fetchall()
tasks = []
for p in pages:
props = json.loads(p["property_values_json"])
tasks.append({
"id": p["id"], "title": p["title"], "icon": p["icon"],
"properties": props,
})
if tasks:
result[col["name"]] = tasks
return {"tasks": result, "total": sum(len(t) for t in result.values())}