task.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. """
  2. Task API router.
  3. Provides CRUD endpoints for task management.
  4. """
  5. import uuid
  6. import json
  7. from typing import List, Optional
  8. from fastapi import APIRouter, HTTPException, status, Query, Request
  9. from database import get_db_connection
  10. from schemas.task import TaskCreate, TaskUpdate, TaskResponse
  11. from models import Task
  12. router = APIRouter(
  13. prefix="/api/tasks",
  14. tags=["tasks"]
  15. )
  16. @router.get("", response_model=List[TaskResponse])
  17. async def list_tasks(
  18. request: Request,
  19. project_id: Optional[str] = Query(None, description="Filter by project ID"),
  20. status_filter: Optional[str] = Query(None, alias="status", description="Filter by status"),
  21. assigned_to: Optional[str] = Query(None, description="Filter by assigned user")
  22. ):
  23. """
  24. List all tasks with optional filters.
  25. Args:
  26. request: FastAPI Request object (contains user info)
  27. project_id: Optional project ID filter
  28. status_filter: Optional status filter (pending, in_progress, completed)
  29. assigned_to: Optional assigned user filter
  30. Returns:
  31. List of tasks matching the filters
  32. Requires authentication.
  33. """
  34. with get_db_connection() as conn:
  35. cursor = conn.cursor()
  36. # Build query with filters
  37. query = """
  38. SELECT
  39. t.id,
  40. t.project_id,
  41. t.name,
  42. t.data,
  43. t.status,
  44. t.assigned_to,
  45. t.created_at,
  46. COALESCE(
  47. CAST(COUNT(a.id) AS FLOAT) / NULLIF(
  48. (SELECT COUNT(*) FROM json_each(t.data, '$.items')),
  49. 0
  50. ),
  51. 0.0
  52. ) as progress
  53. FROM tasks t
  54. LEFT JOIN annotations a ON t.id = a.task_id
  55. WHERE 1=1
  56. """
  57. params = []
  58. if project_id:
  59. query += " AND t.project_id = ?"
  60. params.append(project_id)
  61. if status_filter:
  62. query += " AND t.status = ?"
  63. params.append(status_filter)
  64. if assigned_to:
  65. query += " AND t.assigned_to = ?"
  66. params.append(assigned_to)
  67. query += " GROUP BY t.id ORDER BY t.created_at DESC"
  68. cursor.execute(query, params)
  69. rows = cursor.fetchall()
  70. tasks = []
  71. for row in rows:
  72. # Parse JSON data
  73. data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
  74. tasks.append(TaskResponse(
  75. id=row["id"],
  76. project_id=row["project_id"],
  77. name=row["name"],
  78. data=data,
  79. status=row["status"],
  80. assigned_to=row["assigned_to"],
  81. created_at=row["created_at"],
  82. progress=row["progress"]
  83. ))
  84. return tasks
  85. @router.post("", response_model=TaskResponse, status_code=status.HTTP_201_CREATED)
  86. async def create_task(request: Request, task: TaskCreate):
  87. """
  88. Create a new task.
  89. Args:
  90. request: FastAPI Request object (contains user info)
  91. task: Task creation data
  92. Returns:
  93. Created task with generated ID
  94. Raises:
  95. HTTPException: 404 if project not found
  96. Requires authentication.
  97. Note: If assigned_to is not provided, the task will be assigned to the current user.
  98. """
  99. # Generate unique ID
  100. task_id = f"task_{uuid.uuid4().hex[:12]}"
  101. # Get current user
  102. user = request.state.user
  103. # Use provided assigned_to or default to current user
  104. assigned_to = task.assigned_to if task.assigned_to else user["id"]
  105. with get_db_connection() as conn:
  106. cursor = conn.cursor()
  107. # Verify project exists
  108. cursor.execute("SELECT id FROM projects WHERE id = ?", (task.project_id,))
  109. if not cursor.fetchone():
  110. raise HTTPException(
  111. status_code=status.HTTP_404_NOT_FOUND,
  112. detail=f"Project with id '{task.project_id}' not found"
  113. )
  114. # Serialize data to JSON
  115. data_json = json.dumps(task.data)
  116. # Insert new task
  117. cursor.execute("""
  118. INSERT INTO tasks (id, project_id, name, data, status, assigned_to)
  119. VALUES (?, ?, ?, ?, 'pending', ?)
  120. """, (
  121. task_id,
  122. task.project_id,
  123. task.name,
  124. data_json,
  125. assigned_to
  126. ))
  127. # Fetch the created task
  128. cursor.execute("""
  129. SELECT id, project_id, name, data, status, assigned_to, created_at
  130. FROM tasks
  131. WHERE id = ?
  132. """, (task_id,))
  133. row = cursor.fetchone()
  134. # Parse JSON data
  135. data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
  136. return TaskResponse(
  137. id=row["id"],
  138. project_id=row["project_id"],
  139. name=row["name"],
  140. data=data,
  141. status=row["status"],
  142. assigned_to=row["assigned_to"],
  143. created_at=row["created_at"],
  144. progress=0.0
  145. )
  146. @router.get("/{task_id}", response_model=TaskResponse)
  147. async def get_task(request: Request, task_id: str):
  148. """
  149. Get task by ID.
  150. Args:
  151. request: FastAPI Request object (contains user info)
  152. task_id: Task unique identifier
  153. Returns:
  154. Task details with progress
  155. Raises:
  156. HTTPException: 404 if task not found
  157. Requires authentication.
  158. """
  159. with get_db_connection() as conn:
  160. cursor = conn.cursor()
  161. # Get task with progress
  162. cursor.execute("""
  163. SELECT
  164. t.id,
  165. t.project_id,
  166. t.name,
  167. t.data,
  168. t.status,
  169. t.assigned_to,
  170. t.created_at,
  171. COALESCE(
  172. CAST(COUNT(a.id) AS FLOAT) / NULLIF(
  173. (SELECT COUNT(*) FROM json_each(t.data, '$.items')),
  174. 0
  175. ),
  176. 0.0
  177. ) as progress
  178. FROM tasks t
  179. LEFT JOIN annotations a ON t.id = a.task_id
  180. WHERE t.id = ?
  181. GROUP BY t.id
  182. """, (task_id,))
  183. row = cursor.fetchone()
  184. if not row:
  185. raise HTTPException(
  186. status_code=status.HTTP_404_NOT_FOUND,
  187. detail=f"Task with id '{task_id}' not found"
  188. )
  189. # Parse JSON data
  190. data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
  191. return TaskResponse(
  192. id=row["id"],
  193. project_id=row["project_id"],
  194. name=row["name"],
  195. data=data,
  196. status=row["status"],
  197. assigned_to=row["assigned_to"],
  198. created_at=row["created_at"],
  199. progress=row["progress"]
  200. )
  201. @router.put("/{task_id}", response_model=TaskResponse)
  202. async def update_task(request: Request, task_id: str, task: TaskUpdate):
  203. """
  204. Update an existing task.
  205. Args:
  206. request: FastAPI Request object (contains user info)
  207. task_id: Task unique identifier
  208. task: Task update data
  209. Returns:
  210. Updated task details
  211. Raises:
  212. HTTPException: 404 if task not found
  213. Requires authentication.
  214. """
  215. with get_db_connection() as conn:
  216. cursor = conn.cursor()
  217. # Check if task exists
  218. cursor.execute("SELECT id FROM tasks WHERE id = ?", (task_id,))
  219. if not cursor.fetchone():
  220. raise HTTPException(
  221. status_code=status.HTTP_404_NOT_FOUND,
  222. detail=f"Task with id '{task_id}' not found"
  223. )
  224. # Build update query dynamically based on provided fields
  225. update_fields = []
  226. update_values = []
  227. if task.name is not None:
  228. update_fields.append("name = ?")
  229. update_values.append(task.name)
  230. if task.data is not None:
  231. update_fields.append("data = ?")
  232. update_values.append(json.dumps(task.data))
  233. if task.status is not None:
  234. update_fields.append("status = ?")
  235. update_values.append(task.status)
  236. if task.assigned_to is not None:
  237. update_fields.append("assigned_to = ?")
  238. update_values.append(task.assigned_to)
  239. if not update_fields:
  240. # No fields to update, just return current task
  241. cursor.execute("""
  242. SELECT
  243. t.id,
  244. t.project_id,
  245. t.name,
  246. t.data,
  247. t.status,
  248. t.assigned_to,
  249. t.created_at,
  250. COALESCE(
  251. CAST(COUNT(a.id) AS FLOAT) / NULLIF(
  252. (SELECT COUNT(*) FROM json_each(t.data, '$.items')),
  253. 0
  254. ),
  255. 0.0
  256. ) as progress
  257. FROM tasks t
  258. LEFT JOIN annotations a ON t.id = a.task_id
  259. WHERE t.id = ?
  260. GROUP BY t.id
  261. """, (task_id,))
  262. row = cursor.fetchone()
  263. data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
  264. return TaskResponse(
  265. id=row["id"],
  266. project_id=row["project_id"],
  267. name=row["name"],
  268. data=data,
  269. status=row["status"],
  270. assigned_to=row["assigned_to"],
  271. created_at=row["created_at"],
  272. progress=row["progress"]
  273. )
  274. # Execute update
  275. update_values.append(task_id)
  276. cursor.execute(f"""
  277. UPDATE tasks
  278. SET {', '.join(update_fields)}
  279. WHERE id = ?
  280. """, update_values)
  281. # Fetch and return updated task
  282. cursor.execute("""
  283. SELECT
  284. t.id,
  285. t.project_id,
  286. t.name,
  287. t.data,
  288. t.status,
  289. t.assigned_to,
  290. t.created_at,
  291. COALESCE(
  292. CAST(COUNT(a.id) AS FLOAT) / NULLIF(
  293. (SELECT COUNT(*) FROM json_each(t.data, '$.items')),
  294. 0
  295. ),
  296. 0.0
  297. ) as progress
  298. FROM tasks t
  299. LEFT JOIN annotations a ON t.id = a.task_id
  300. WHERE t.id = ?
  301. GROUP BY t.id
  302. """, (task_id,))
  303. row = cursor.fetchone()
  304. data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
  305. return TaskResponse(
  306. id=row["id"],
  307. project_id=row["project_id"],
  308. name=row["name"],
  309. data=data,
  310. status=row["status"],
  311. assigned_to=row["assigned_to"],
  312. created_at=row["created_at"],
  313. progress=row["progress"]
  314. )
  315. @router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
  316. async def delete_task(request: Request, task_id: str):
  317. """
  318. Delete a task and all associated annotations.
  319. Args:
  320. request: FastAPI Request object (contains user info)
  321. task_id: Task unique identifier
  322. Raises:
  323. HTTPException: 404 if task not found
  324. HTTPException: 403 if user is not admin
  325. Requires authentication and admin role.
  326. """
  327. # Check if user has admin role
  328. user = request.state.user
  329. if user["role"] != "admin":
  330. raise HTTPException(
  331. status_code=status.HTTP_403_FORBIDDEN,
  332. detail="只有管理员可以删除任务"
  333. )
  334. with get_db_connection() as conn:
  335. cursor = conn.cursor()
  336. # Check if task exists
  337. cursor.execute("SELECT id FROM tasks WHERE id = ?", (task_id,))
  338. if not cursor.fetchone():
  339. raise HTTPException(
  340. status_code=status.HTTP_404_NOT_FOUND,
  341. detail=f"Task with id '{task_id}' not found"
  342. )
  343. # Delete task (cascade will delete annotations)
  344. cursor.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
  345. return None
  346. @router.get("/projects/{project_id}/tasks", response_model=List[TaskResponse])
  347. async def get_project_tasks(request: Request, project_id: str):
  348. """
  349. Get all tasks for a specific project.
  350. Args:
  351. request: FastAPI Request object (contains user info)
  352. project_id: Project unique identifier
  353. Returns:
  354. List of tasks belonging to the project
  355. Raises:
  356. HTTPException: 404 if project not found
  357. Requires authentication.
  358. """
  359. with get_db_connection() as conn:
  360. cursor = conn.cursor()
  361. # Verify project exists
  362. cursor.execute("SELECT id FROM projects WHERE id = ?", (project_id,))
  363. if not cursor.fetchone():
  364. raise HTTPException(
  365. status_code=status.HTTP_404_NOT_FOUND,
  366. detail=f"Project with id '{project_id}' not found"
  367. )
  368. # Get all tasks for the project
  369. cursor.execute("""
  370. SELECT
  371. t.id,
  372. t.project_id,
  373. t.name,
  374. t.data,
  375. t.status,
  376. t.assigned_to,
  377. t.created_at,
  378. COALESCE(
  379. CAST(COUNT(a.id) AS FLOAT) / NULLIF(
  380. (SELECT COUNT(*) FROM json_each(t.data, '$.items')),
  381. 0
  382. ),
  383. 0.0
  384. ) as progress
  385. FROM tasks t
  386. LEFT JOIN annotations a ON t.id = a.task_id
  387. WHERE t.project_id = ?
  388. GROUP BY t.id
  389. ORDER BY t.created_at DESC
  390. """, (project_id,))
  391. rows = cursor.fetchall()
  392. tasks = []
  393. for row in rows:
  394. # Parse JSON data
  395. data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
  396. tasks.append(TaskResponse(
  397. id=row["id"],
  398. project_id=row["project_id"],
  399. name=row["name"],
  400. data=data,
  401. status=row["status"],
  402. assigned_to=row["assigned_to"],
  403. created_at=row["created_at"],
  404. progress=row["progress"]
  405. ))
  406. return tasks