Files
anti-plagiarism/services/api/app/api/tasks.py
jze9 29301c3a4c
All checks were successful
Deploy / test (push) Successful in 3m3s
Deploy / deploy (push) Successful in 20s
feat(cabinet): дать пользователю самому удалять задачи из кабинета
Раньше удалить задачу мог только админ через свою панель — обычный
пользователь с зависшей (например, навечно застрявшей в processing)
задачей не мог убрать её из кабинета вообще. Кнопка удаления на
карточке + снято искусственное ограничение "нельзя удалить, пока
processing" (все воркеры уже корректно проверяют task is None перед
записью, удаление во время реальной обработки безопасно).
2026-08-25 15:24:46 +05:00

124 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Управление задачами пользователя.
В URL используется public_id (не внутренний UUID), чтобы не раскрывать
структуру внутренних идентификаторов.
"""
import logging
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.minio_client import get_minio
from app.core.security import get_current_user
from app.database import get_db
from app.models.admin import StagedWork
from app.models.task import Task
from app.models.user import User
from app.schemas.tasks import TaskResponse
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/tasks", tags=["tasks"])
@router.get("/", response_model=list[TaskResponse])
async def list_tasks(
limit: int = 20,
offset: int = 0,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> list[TaskResponse]:
"""Список задач текущего пользователя (новые первыми)."""
# Жёсткий потолок limit — не даём вытащить всю таблицу одним запросом
limit = min(limit, 100)
result = await db.execute(
select(Task)
.where(Task.user_id == current_user.id)
.order_by(Task.created_at.desc())
.limit(limit)
.offset(offset)
)
return [TaskResponse.model_validate(t) for t in result.scalars().all()]
@router.get("/{public_id}", response_model=TaskResponse)
async def get_task(
public_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> TaskResponse:
"""Получить задачу по публичному ID. Возвращает 404 если задача чужая — не раскрываем факт существования."""
result = await db.execute(
select(Task).where(
Task.public_id == public_id,
Task.user_id == current_user.id, # ownership проверяется в одном запросе
)
)
task = result.scalar_one_or_none()
if task is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Задача не найдена")
return TaskResponse.model_validate(task)
@router.get("/{public_id}/text")
async def get_task_text(
public_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> dict:
"""Полный текст проверенного документа — для подсветки совпадений в интерфейсе.
Тот же текст, по которому worker-gpu считал position_start/position_end в
отчёте о плагиате (см. StagedWork.text_key — пишется в _stage_work при
извлечении, worker-indexer/app/tasks/index.py). Запись туда — best-effort
(сбой не валит саму проверку), поэтому текст доступен не для всех задач.
"""
result = await db.execute(
select(Task).where(Task.public_id == public_id, Task.user_id == current_user.id)
)
task = result.scalar_one_or_none()
if task is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Задача не найдена")
staged = (
await db.execute(select(StagedWork).where(StagedWork.task_id == task.id))
).scalar_one_or_none()
if staged is None or not staged.text_key:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Текст работы недоступен")
try:
obj = get_minio().get_object("staging", staged.text_key)
text = obj.read().decode("utf-8", errors="replace")
except Exception as e:
logger.warning(f"Не удалось прочитать текст задачи {public_id!r}: {e}")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Текст работы недоступен") from e
return {"text": text}
@router.delete("/{public_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_task(
public_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> None:
"""Удалить задачу. Нельзя удалить задачу в статусе 'processing'."""
result = await db.execute(
select(Task).where(
Task.public_id == public_id,
Task.user_id == current_user.id,
)
)
task = result.scalar_one_or_none()
if task is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Задача не найдена")
await db.delete(task)
await db.commit()