task.py 13 KB

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