project.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. """
  2. Project API router.
  3. Provides CRUD endpoints for project management.
  4. """
  5. import uuid
  6. from typing import List
  7. from fastapi import APIRouter, HTTPException, status, Request
  8. from database import get_db_connection
  9. from schemas.project import ProjectCreate, ProjectUpdate, ProjectResponse
  10. from models import Project
  11. router = APIRouter(
  12. prefix="/api/projects",
  13. tags=["projects"]
  14. )
  15. @router.get("", response_model=List[ProjectResponse])
  16. async def list_projects(request: Request):
  17. """
  18. List all projects.
  19. Returns a list of all projects with their task counts.
  20. Requires authentication.
  21. """
  22. with get_db_connection() as conn:
  23. cursor = conn.cursor()
  24. # Get all projects with task counts
  25. cursor.execute("""
  26. SELECT
  27. p.id,
  28. p.name,
  29. p.description,
  30. p.config,
  31. p.created_at,
  32. COUNT(t.id) as task_count
  33. FROM projects p
  34. LEFT JOIN tasks t ON p.id = t.project_id
  35. GROUP BY p.id
  36. ORDER BY p.created_at DESC
  37. """)
  38. rows = cursor.fetchall()
  39. projects = []
  40. for row in rows:
  41. projects.append(ProjectResponse(
  42. id=row["id"],
  43. name=row["name"],
  44. description=row["description"] or "",
  45. config=row["config"],
  46. created_at=row["created_at"],
  47. task_count=row["task_count"]
  48. ))
  49. return projects
  50. @router.post("", response_model=ProjectResponse, status_code=status.HTTP_201_CREATED)
  51. async def create_project(request: Request, project: ProjectCreate):
  52. """
  53. Create a new project.
  54. Args:
  55. request: FastAPI Request object (contains user info)
  56. project: Project creation data
  57. Returns:
  58. Created project with generated ID
  59. Requires authentication.
  60. """
  61. # Generate unique ID
  62. project_id = f"proj_{uuid.uuid4().hex[:12]}"
  63. with get_db_connection() as conn:
  64. cursor = conn.cursor()
  65. # Insert new project
  66. cursor.execute("""
  67. INSERT INTO projects (id, name, description, config)
  68. VALUES (?, ?, ?, ?)
  69. """, (
  70. project_id,
  71. project.name,
  72. project.description,
  73. project.config
  74. ))
  75. # Fetch the created project
  76. cursor.execute("""
  77. SELECT id, name, description, config, created_at
  78. FROM projects
  79. WHERE id = ?
  80. """, (project_id,))
  81. row = cursor.fetchone()
  82. return ProjectResponse(
  83. id=row["id"],
  84. name=row["name"],
  85. description=row["description"] or "",
  86. config=row["config"],
  87. created_at=row["created_at"],
  88. task_count=0
  89. )
  90. @router.get("/{project_id}", response_model=ProjectResponse)
  91. async def get_project(request: Request, project_id: str):
  92. """
  93. Get project by ID.
  94. Args:
  95. request: FastAPI Request object (contains user info)
  96. project_id: Project unique identifier
  97. Returns:
  98. Project details with task count
  99. Raises:
  100. HTTPException: 404 if project not found
  101. Requires authentication.
  102. """
  103. with get_db_connection() as conn:
  104. cursor = conn.cursor()
  105. # Get project with task count
  106. cursor.execute("""
  107. SELECT
  108. p.id,
  109. p.name,
  110. p.description,
  111. p.config,
  112. p.created_at,
  113. COUNT(t.id) as task_count
  114. FROM projects p
  115. LEFT JOIN tasks t ON p.id = t.project_id
  116. WHERE p.id = ?
  117. GROUP BY p.id
  118. """, (project_id,))
  119. row = cursor.fetchone()
  120. if not row:
  121. raise HTTPException(
  122. status_code=status.HTTP_404_NOT_FOUND,
  123. detail=f"Project with id '{project_id}' not found"
  124. )
  125. return ProjectResponse(
  126. id=row["id"],
  127. name=row["name"],
  128. description=row["description"] or "",
  129. config=row["config"],
  130. created_at=row["created_at"],
  131. task_count=row["task_count"]
  132. )
  133. @router.put("/{project_id}", response_model=ProjectResponse)
  134. async def update_project(request: Request, project_id: str, project: ProjectUpdate):
  135. """
  136. Update an existing project.
  137. Args:
  138. request: FastAPI Request object (contains user info)
  139. project_id: Project unique identifier
  140. project: Project update data
  141. Returns:
  142. Updated project details
  143. Raises:
  144. HTTPException: 404 if project not found
  145. Requires authentication.
  146. """
  147. with get_db_connection() as conn:
  148. cursor = conn.cursor()
  149. # Check if project exists
  150. cursor.execute("SELECT id FROM projects WHERE id = ?", (project_id,))
  151. if not cursor.fetchone():
  152. raise HTTPException(
  153. status_code=status.HTTP_404_NOT_FOUND,
  154. detail=f"Project with id '{project_id}' not found"
  155. )
  156. # Build update query dynamically based on provided fields
  157. update_fields = []
  158. update_values = []
  159. if project.name is not None:
  160. update_fields.append("name = ?")
  161. update_values.append(project.name)
  162. if project.description is not None:
  163. update_fields.append("description = ?")
  164. update_values.append(project.description)
  165. if project.config is not None:
  166. update_fields.append("config = ?")
  167. update_values.append(project.config)
  168. if not update_fields:
  169. # No fields to update, just return current project
  170. cursor.execute("""
  171. SELECT
  172. p.id,
  173. p.name,
  174. p.description,
  175. p.config,
  176. p.created_at,
  177. COUNT(t.id) as task_count
  178. FROM projects p
  179. LEFT JOIN tasks t ON p.id = t.project_id
  180. WHERE p.id = ?
  181. GROUP BY p.id
  182. """, (project_id,))
  183. row = cursor.fetchone()
  184. return ProjectResponse(
  185. id=row["id"],
  186. name=row["name"],
  187. description=row["description"] or "",
  188. config=row["config"],
  189. created_at=row["created_at"],
  190. task_count=row["task_count"]
  191. )
  192. # Execute update
  193. update_values.append(project_id)
  194. cursor.execute(f"""
  195. UPDATE projects
  196. SET {', '.join(update_fields)}
  197. WHERE id = ?
  198. """, update_values)
  199. # Fetch and return updated project
  200. cursor.execute("""
  201. SELECT
  202. p.id,
  203. p.name,
  204. p.description,
  205. p.config,
  206. p.created_at,
  207. COUNT(t.id) as task_count
  208. FROM projects p
  209. LEFT JOIN tasks t ON p.id = t.project_id
  210. WHERE p.id = ?
  211. GROUP BY p.id
  212. """, (project_id,))
  213. row = cursor.fetchone()
  214. return ProjectResponse(
  215. id=row["id"],
  216. name=row["name"],
  217. description=row["description"] or "",
  218. config=row["config"],
  219. created_at=row["created_at"],
  220. task_count=row["task_count"]
  221. )
  222. @router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
  223. async def delete_project(request: Request, project_id: str):
  224. """
  225. Delete a project and all associated tasks.
  226. Args:
  227. request: FastAPI Request object (contains user info)
  228. project_id: Project unique identifier
  229. Raises:
  230. HTTPException: 404 if project not found
  231. HTTPException: 403 if user is not admin
  232. Requires authentication and admin role.
  233. """
  234. # Check if user has admin role
  235. user = request.state.user
  236. if user["role"] != "admin":
  237. raise HTTPException(
  238. status_code=status.HTTP_403_FORBIDDEN,
  239. detail="只有管理员可以删除项目"
  240. )
  241. with get_db_connection() as conn:
  242. cursor = conn.cursor()
  243. # Check if project exists
  244. cursor.execute("SELECT id FROM projects WHERE id = ?", (project_id,))
  245. if not cursor.fetchone():
  246. raise HTTPException(
  247. status_code=status.HTTP_404_NOT_FOUND,
  248. detail=f"Project with id '{project_id}' not found"
  249. )
  250. # Delete project (cascade will delete tasks and annotations)
  251. cursor.execute("DELETE FROM projects WHERE id = ?", (project_id,))
  252. return None