|
|
@@ -16,6 +16,18 @@ router = APIRouter(
|
|
|
)
|
|
|
|
|
|
|
|
|
+def calculate_progress(data_str: str, annotation_count: int) -> float:
|
|
|
+ """计算任务进度"""
|
|
|
+ try:
|
|
|
+ data = json.loads(data_str) if isinstance(data_str, str) else data_str
|
|
|
+ items = data.get('items', [])
|
|
|
+ if not items:
|
|
|
+ return 0.0
|
|
|
+ return min(annotation_count / len(items), 1.0)
|
|
|
+ except:
|
|
|
+ return 0.0
|
|
|
+
|
|
|
+
|
|
|
@router.get("", response_model=List[TaskResponse])
|
|
|
async def list_tasks(
|
|
|
request: Request,
|
|
|
@@ -25,16 +37,6 @@ async def list_tasks(
|
|
|
):
|
|
|
"""
|
|
|
List all tasks with optional filters.
|
|
|
-
|
|
|
- Args:
|
|
|
- request: FastAPI Request object (contains user info)
|
|
|
- project_id: Optional project ID filter
|
|
|
- status_filter: Optional status filter (pending, in_progress, completed)
|
|
|
- assigned_to: Optional assigned user filter
|
|
|
-
|
|
|
- Returns:
|
|
|
- List of tasks matching the filters
|
|
|
-
|
|
|
Requires authentication.
|
|
|
"""
|
|
|
with get_db_connection() as conn:
|
|
|
@@ -50,13 +52,7 @@ async def list_tasks(
|
|
|
t.status,
|
|
|
t.assigned_to,
|
|
|
t.created_at,
|
|
|
- COALESCE(
|
|
|
- CAST(COUNT(a.id) AS FLOAT) / NULLIF(
|
|
|
- (SELECT COUNT(*) FROM json_each(t.data, '$.items')),
|
|
|
- 0
|
|
|
- ),
|
|
|
- 0.0
|
|
|
- ) as progress
|
|
|
+ COUNT(a.id) as annotation_count
|
|
|
FROM tasks t
|
|
|
LEFT JOIN annotations a ON t.id = a.task_id
|
|
|
WHERE 1=1
|
|
|
@@ -75,15 +71,15 @@ async def list_tasks(
|
|
|
query += " AND t.assigned_to = ?"
|
|
|
params.append(assigned_to)
|
|
|
|
|
|
- query += " GROUP BY t.id ORDER BY t.created_at DESC"
|
|
|
+ query += " GROUP BY t.id, t.project_id, t.name, t.data, t.status, t.assigned_to, t.created_at ORDER BY t.created_at DESC"
|
|
|
|
|
|
- cursor.execute(query, params)
|
|
|
+ cursor.execute(query, tuple(params))
|
|
|
rows = cursor.fetchall()
|
|
|
|
|
|
tasks = []
|
|
|
for row in rows:
|
|
|
- # Parse JSON data
|
|
|
data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
|
|
|
+ progress = calculate_progress(row["data"], row["annotation_count"])
|
|
|
|
|
|
tasks.append(TaskResponse(
|
|
|
id=row["id"],
|
|
|
@@ -93,7 +89,7 @@ async def list_tasks(
|
|
|
status=row["status"],
|
|
|
assigned_to=row["assigned_to"],
|
|
|
created_at=row["created_at"],
|
|
|
- progress=row["progress"]
|
|
|
+ progress=progress
|
|
|
))
|
|
|
|
|
|
return tasks
|
|
|
@@ -103,27 +99,10 @@ async def list_tasks(
|
|
|
async def create_task(request: Request, task: TaskCreate):
|
|
|
"""
|
|
|
Create a new task.
|
|
|
-
|
|
|
- Args:
|
|
|
- request: FastAPI Request object (contains user info)
|
|
|
- task: Task creation data
|
|
|
-
|
|
|
- Returns:
|
|
|
- Created task with generated ID
|
|
|
-
|
|
|
- Raises:
|
|
|
- HTTPException: 404 if project not found
|
|
|
-
|
|
|
Requires authentication.
|
|
|
- Note: If assigned_to is not provided, the task will be assigned to the current user.
|
|
|
"""
|
|
|
- # Generate unique ID
|
|
|
task_id = f"task_{uuid.uuid4().hex[:12]}"
|
|
|
-
|
|
|
- # Get current user
|
|
|
user = request.state.user
|
|
|
-
|
|
|
- # Use provided assigned_to or default to current user
|
|
|
assigned_to = task.assigned_to if task.assigned_to else user["id"]
|
|
|
|
|
|
with get_db_connection() as conn:
|
|
|
@@ -137,31 +116,19 @@ async def create_task(request: Request, task: TaskCreate):
|
|
|
detail=f"Project with id '{task.project_id}' not found"
|
|
|
)
|
|
|
|
|
|
- # Serialize data to JSON
|
|
|
data_json = json.dumps(task.data)
|
|
|
|
|
|
- # Insert new task
|
|
|
cursor.execute("""
|
|
|
INSERT INTO tasks (id, project_id, name, data, status, assigned_to)
|
|
|
VALUES (?, ?, ?, ?, 'pending', ?)
|
|
|
- """, (
|
|
|
- task_id,
|
|
|
- task.project_id,
|
|
|
- task.name,
|
|
|
- data_json,
|
|
|
- assigned_to
|
|
|
- ))
|
|
|
-
|
|
|
- # Fetch the created task
|
|
|
+ """, (task_id, task.project_id, task.name, data_json, assigned_to))
|
|
|
+
|
|
|
cursor.execute("""
|
|
|
SELECT id, project_id, name, data, status, assigned_to, created_at
|
|
|
- FROM tasks
|
|
|
- WHERE id = ?
|
|
|
+ FROM tasks WHERE id = ?
|
|
|
""", (task_id,))
|
|
|
|
|
|
row = cursor.fetchone()
|
|
|
-
|
|
|
- # Parse JSON data
|
|
|
data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
|
|
|
|
|
|
return TaskResponse(
|
|
|
@@ -180,23 +147,11 @@ async def create_task(request: Request, task: TaskCreate):
|
|
|
async def get_task(request: Request, task_id: str):
|
|
|
"""
|
|
|
Get task by ID.
|
|
|
-
|
|
|
- Args:
|
|
|
- request: FastAPI Request object (contains user info)
|
|
|
- task_id: Task unique identifier
|
|
|
-
|
|
|
- Returns:
|
|
|
- Task details with progress
|
|
|
-
|
|
|
- Raises:
|
|
|
- HTTPException: 404 if task not found
|
|
|
-
|
|
|
Requires authentication.
|
|
|
"""
|
|
|
with get_db_connection() as conn:
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
- # Get task with progress
|
|
|
cursor.execute("""
|
|
|
SELECT
|
|
|
t.id,
|
|
|
@@ -206,17 +161,11 @@ async def get_task(request: Request, task_id: str):
|
|
|
t.status,
|
|
|
t.assigned_to,
|
|
|
t.created_at,
|
|
|
- COALESCE(
|
|
|
- CAST(COUNT(a.id) AS FLOAT) / NULLIF(
|
|
|
- (SELECT COUNT(*) FROM json_each(t.data, '$.items')),
|
|
|
- 0
|
|
|
- ),
|
|
|
- 0.0
|
|
|
- ) as progress
|
|
|
+ COUNT(a.id) as annotation_count
|
|
|
FROM tasks t
|
|
|
LEFT JOIN annotations a ON t.id = a.task_id
|
|
|
WHERE t.id = ?
|
|
|
- GROUP BY t.id
|
|
|
+ GROUP BY t.id, t.project_id, t.name, t.data, t.status, t.assigned_to, t.created_at
|
|
|
""", (task_id,))
|
|
|
|
|
|
row = cursor.fetchone()
|
|
|
@@ -227,8 +176,8 @@ async def get_task(request: Request, task_id: str):
|
|
|
detail=f"Task with id '{task_id}' not found"
|
|
|
)
|
|
|
|
|
|
- # Parse JSON data
|
|
|
data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
|
|
|
+ progress = calculate_progress(row["data"], row["annotation_count"])
|
|
|
|
|
|
return TaskResponse(
|
|
|
id=row["id"],
|
|
|
@@ -238,7 +187,7 @@ async def get_task(request: Request, task_id: str):
|
|
|
status=row["status"],
|
|
|
assigned_to=row["assigned_to"],
|
|
|
created_at=row["created_at"],
|
|
|
- progress=row["progress"]
|
|
|
+ progress=progress
|
|
|
)
|
|
|
|
|
|
|
|
|
@@ -246,24 +195,11 @@ async def get_task(request: Request, task_id: str):
|
|
|
async def update_task(request: Request, task_id: str, task: TaskUpdate):
|
|
|
"""
|
|
|
Update an existing task.
|
|
|
-
|
|
|
- Args:
|
|
|
- request: FastAPI Request object (contains user info)
|
|
|
- task_id: Task unique identifier
|
|
|
- task: Task update data
|
|
|
-
|
|
|
- Returns:
|
|
|
- Updated task details
|
|
|
-
|
|
|
- Raises:
|
|
|
- HTTPException: 404 if task not found
|
|
|
-
|
|
|
Requires authentication.
|
|
|
"""
|
|
|
with get_db_connection() as conn:
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
- # Check if task exists
|
|
|
cursor.execute("SELECT id FROM tasks WHERE id = ?", (task_id,))
|
|
|
if not cursor.fetchone():
|
|
|
raise HTTPException(
|
|
|
@@ -271,7 +207,6 @@ async def update_task(request: Request, task_id: str, task: TaskUpdate):
|
|
|
detail=f"Task with id '{task_id}' not found"
|
|
|
)
|
|
|
|
|
|
- # Build update query dynamically based on provided fields
|
|
|
update_fields = []
|
|
|
update_values = []
|
|
|
|
|
|
@@ -291,75 +226,26 @@ async def update_task(request: Request, task_id: str, task: TaskUpdate):
|
|
|
update_fields.append("assigned_to = ?")
|
|
|
update_values.append(task.assigned_to)
|
|
|
|
|
|
- if not update_fields:
|
|
|
- # No fields to update, just return current task
|
|
|
- cursor.execute("""
|
|
|
- SELECT
|
|
|
- t.id,
|
|
|
- t.project_id,
|
|
|
- t.name,
|
|
|
- t.data,
|
|
|
- t.status,
|
|
|
- t.assigned_to,
|
|
|
- t.created_at,
|
|
|
- COALESCE(
|
|
|
- CAST(COUNT(a.id) AS FLOAT) / NULLIF(
|
|
|
- (SELECT COUNT(*) FROM json_each(t.data, '$.items')),
|
|
|
- 0
|
|
|
- ),
|
|
|
- 0.0
|
|
|
- ) as progress
|
|
|
- FROM tasks t
|
|
|
- LEFT JOIN annotations a ON t.id = a.task_id
|
|
|
- WHERE t.id = ?
|
|
|
- GROUP BY t.id
|
|
|
- """, (task_id,))
|
|
|
- row = cursor.fetchone()
|
|
|
- data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
|
|
|
- return TaskResponse(
|
|
|
- id=row["id"],
|
|
|
- project_id=row["project_id"],
|
|
|
- name=row["name"],
|
|
|
- data=data,
|
|
|
- status=row["status"],
|
|
|
- assigned_to=row["assigned_to"],
|
|
|
- created_at=row["created_at"],
|
|
|
- progress=row["progress"]
|
|
|
- )
|
|
|
+ if update_fields:
|
|
|
+ update_values.append(task_id)
|
|
|
+ cursor.execute(f"""
|
|
|
+ UPDATE tasks SET {', '.join(update_fields)} WHERE id = ?
|
|
|
+ """, tuple(update_values))
|
|
|
|
|
|
- # Execute update
|
|
|
- update_values.append(task_id)
|
|
|
- cursor.execute(f"""
|
|
|
- UPDATE tasks
|
|
|
- SET {', '.join(update_fields)}
|
|
|
- WHERE id = ?
|
|
|
- """, update_values)
|
|
|
-
|
|
|
- # Fetch and return updated task
|
|
|
cursor.execute("""
|
|
|
SELECT
|
|
|
- t.id,
|
|
|
- t.project_id,
|
|
|
- t.name,
|
|
|
- t.data,
|
|
|
- t.status,
|
|
|
- t.assigned_to,
|
|
|
- t.created_at,
|
|
|
- COALESCE(
|
|
|
- CAST(COUNT(a.id) AS FLOAT) / NULLIF(
|
|
|
- (SELECT COUNT(*) FROM json_each(t.data, '$.items')),
|
|
|
- 0
|
|
|
- ),
|
|
|
- 0.0
|
|
|
- ) as progress
|
|
|
+ t.id, t.project_id, t.name, t.data, t.status, t.assigned_to, t.created_at,
|
|
|
+ COUNT(a.id) as annotation_count
|
|
|
FROM tasks t
|
|
|
LEFT JOIN annotations a ON t.id = a.task_id
|
|
|
WHERE t.id = ?
|
|
|
- GROUP BY t.id
|
|
|
+ GROUP BY t.id, t.project_id, t.name, t.data, t.status, t.assigned_to, t.created_at
|
|
|
""", (task_id,))
|
|
|
|
|
|
row = cursor.fetchone()
|
|
|
data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
|
|
|
+ progress = calculate_progress(row["data"], row["annotation_count"])
|
|
|
+
|
|
|
return TaskResponse(
|
|
|
id=row["id"],
|
|
|
project_id=row["project_id"],
|
|
|
@@ -368,7 +254,7 @@ async def update_task(request: Request, task_id: str, task: TaskUpdate):
|
|
|
status=row["status"],
|
|
|
assigned_to=row["assigned_to"],
|
|
|
created_at=row["created_at"],
|
|
|
- progress=row["progress"]
|
|
|
+ progress=progress
|
|
|
)
|
|
|
|
|
|
|
|
|
@@ -376,18 +262,8 @@ async def update_task(request: Request, task_id: str, task: TaskUpdate):
|
|
|
async def delete_task(request: Request, task_id: str):
|
|
|
"""
|
|
|
Delete a task and all associated annotations.
|
|
|
-
|
|
|
- Args:
|
|
|
- request: FastAPI Request object (contains user info)
|
|
|
- task_id: Task unique identifier
|
|
|
-
|
|
|
- Raises:
|
|
|
- HTTPException: 404 if task not found
|
|
|
- HTTPException: 403 if user is not admin
|
|
|
-
|
|
|
Requires authentication and admin role.
|
|
|
"""
|
|
|
- # Check if user has admin role
|
|
|
user = request.state.user
|
|
|
if user["role"] != "admin":
|
|
|
raise HTTPException(
|
|
|
@@ -398,7 +274,6 @@ async def delete_task(request: Request, task_id: str):
|
|
|
with get_db_connection() as conn:
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
- # Check if task exists
|
|
|
cursor.execute("SELECT id FROM tasks WHERE id = ?", (task_id,))
|
|
|
if not cursor.fetchone():
|
|
|
raise HTTPException(
|
|
|
@@ -406,9 +281,7 @@ async def delete_task(request: Request, task_id: str):
|
|
|
detail=f"Task with id '{task_id}' not found"
|
|
|
)
|
|
|
|
|
|
- # Delete task (cascade will delete annotations)
|
|
|
cursor.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
|
|
|
-
|
|
|
return None
|
|
|
|
|
|
|
|
|
@@ -416,23 +289,11 @@ async def delete_task(request: Request, task_id: str):
|
|
|
async def get_project_tasks(request: Request, project_id: str):
|
|
|
"""
|
|
|
Get all tasks for a specific project.
|
|
|
-
|
|
|
- Args:
|
|
|
- request: FastAPI Request object (contains user info)
|
|
|
- project_id: Project unique identifier
|
|
|
-
|
|
|
- Returns:
|
|
|
- List of tasks belonging to the project
|
|
|
-
|
|
|
- Raises:
|
|
|
- HTTPException: 404 if project not found
|
|
|
-
|
|
|
Requires authentication.
|
|
|
"""
|
|
|
with get_db_connection() as conn:
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
- # Verify project exists
|
|
|
cursor.execute("SELECT id FROM projects WHERE id = ?", (project_id,))
|
|
|
if not cursor.fetchone():
|
|
|
raise HTTPException(
|
|
|
@@ -440,27 +301,14 @@ async def get_project_tasks(request: Request, project_id: str):
|
|
|
detail=f"Project with id '{project_id}' not found"
|
|
|
)
|
|
|
|
|
|
- # Get all tasks for the project
|
|
|
cursor.execute("""
|
|
|
SELECT
|
|
|
- t.id,
|
|
|
- t.project_id,
|
|
|
- t.name,
|
|
|
- t.data,
|
|
|
- t.status,
|
|
|
- t.assigned_to,
|
|
|
- t.created_at,
|
|
|
- COALESCE(
|
|
|
- CAST(COUNT(a.id) AS FLOAT) / NULLIF(
|
|
|
- (SELECT COUNT(*) FROM json_each(t.data, '$.items')),
|
|
|
- 0
|
|
|
- ),
|
|
|
- 0.0
|
|
|
- ) as progress
|
|
|
+ t.id, t.project_id, t.name, t.data, t.status, t.assigned_to, t.created_at,
|
|
|
+ COUNT(a.id) as annotation_count
|
|
|
FROM tasks t
|
|
|
LEFT JOIN annotations a ON t.id = a.task_id
|
|
|
WHERE t.project_id = ?
|
|
|
- GROUP BY t.id
|
|
|
+ GROUP BY t.id, t.project_id, t.name, t.data, t.status, t.assigned_to, t.created_at
|
|
|
ORDER BY t.created_at DESC
|
|
|
""", (project_id,))
|
|
|
|
|
|
@@ -468,8 +316,8 @@ async def get_project_tasks(request: Request, project_id: str):
|
|
|
|
|
|
tasks = []
|
|
|
for row in rows:
|
|
|
- # Parse JSON data
|
|
|
data = json.loads(row["data"]) if isinstance(row["data"], str) else row["data"]
|
|
|
+ progress = calculate_progress(row["data"], row["annotation_count"])
|
|
|
|
|
|
tasks.append(TaskResponse(
|
|
|
id=row["id"],
|
|
|
@@ -479,7 +327,7 @@ async def get_project_tasks(request: Request, project_id: str):
|
|
|
status=row["status"],
|
|
|
assigned_to=row["assigned_to"],
|
|
|
created_at=row["created_at"],
|
|
|
- progress=row["progress"]
|
|
|
+ progress=progress
|
|
|
))
|
|
|
|
|
|
return tasks
|