diff --git a/backend/app/api/v1/routes_language_qc.py b/backend/app/api/v1/routes_language_qc.py index e3c60c0..6fc0dfe 100644 --- a/backend/app/api/v1/routes_language_qc.py +++ b/backend/app/api/v1/routes_language_qc.py @@ -8,9 +8,11 @@ from pydantic import BaseModel, Field from ...core.database import get_database from ...core.dependencies import require_roles +from ...models.audit_log import AuditAction from ...models.job import LanguageQCComment, LanguageQCState from ...models.user import User, UserRole from ...services import language_qc as lqc +from ...services.audit_logger import audit_logger router = APIRouter(tags=["language-qc"]) @@ -131,6 +133,15 @@ async def assign_language( db, job_id, lang, request.linguist_user_id, current_user, http_request=http_request, notes=request.notes, deadline=request.deadline, ) + await audit_logger.log_action( + action=AuditAction.LANGUAGE_QC_ASSIGN, + description=f"Language '{lang}' assigned to linguist '{request.linguist_user_id}' for job {job_id}", + user=current_user, + request=http_request, + resource_type="job", + resource_id=job_id, + details={"lang": lang, "linguist_user_id": request.linguist_user_id}, + ) return LanguageQCStateResponse(lang=lang, state=state) @@ -149,6 +160,15 @@ async def reassign_language( db, job_id, lang, request.linguist_user_id, current_user, http_request=http_request, notes=request.notes, deadline=request.deadline, ) + await audit_logger.log_action( + action=AuditAction.LANGUAGE_QC_REASSIGN, + description=f"Language '{lang}' reassigned to linguist '{request.linguist_user_id}' for job {job_id}", + user=current_user, + request=http_request, + resource_type="job", + resource_id=job_id, + details={"lang": lang, "linguist_user_id": request.linguist_user_id}, + ) return LanguageQCStateResponse(lang=lang, state=state) @@ -169,6 +189,15 @@ async def assign_reviewer( db, job_id, lang, request.reviewer_user_id, current_user, http_request=http_request, notes=request.notes, deadline=request.deadline, ) + await audit_logger.log_action( + action=AuditAction.LANGUAGE_QC_REVIEWER_ASSIGN, + description=f"Reviewer '{request.reviewer_user_id}' assigned to language '{lang}' for job {job_id}", + user=current_user, + request=http_request, + resource_type="job", + resource_id=job_id, + details={"lang": lang, "reviewer_user_id": request.reviewer_user_id}, + ) return LanguageQCStateResponse(lang=lang, state=state) @@ -187,6 +216,15 @@ async def reassign_reviewer( db, job_id, lang, request.reviewer_user_id, current_user, http_request=http_request, notes=request.notes, deadline=request.deadline, ) + await audit_logger.log_action( + action=AuditAction.LANGUAGE_QC_REVIEWER_REASSIGN, + description=f"Reviewer reassigned to '{request.reviewer_user_id}' for language '{lang}', job {job_id}", + user=current_user, + request=http_request, + resource_type="job", + resource_id=job_id, + details={"lang": lang, "reviewer_user_id": request.reviewer_user_id}, + ) return LanguageQCStateResponse(lang=lang, state=state) @@ -248,6 +286,21 @@ async def bulk_assign_languages( assigned.append(lang) + await audit_logger.log_action( + action=AuditAction.LANGUAGE_QC_BULK_ASSIGN, + description=f"Bulk assignment for job {job_id}: {len(assigned)} language(s) assigned to linguist '{request.linguist_user_id}'", + user=current_user, + request=http_request, + resource_type="job", + resource_id=job_id, + details={ + "languages": assigned, + "linguist_user_id": request.linguist_user_id, + "reviewer_user_id": request.reviewer_user_id, + "skipped": skipped, + "errors": errors, + }, + ) return BulkAssignResponse(assigned=assigned, skipped=skipped, errors=errors) @@ -265,6 +318,15 @@ async def start_linguist_work( ): """Linguist opens the language — pending → in_progress.""" state = await lqc.start_linguist_work(db, job_id, lang, current_user) + await audit_logger.log_action( + action=AuditAction.LANGUAGE_QC_START_WORK, + description=f"Linguist started work on language '{lang}' for job {job_id}", + user=current_user, + request=http_request, + resource_type="job", + resource_id=job_id, + details={"lang": lang}, + ) return LanguageQCStateResponse(lang=lang, state=state) @@ -280,6 +342,15 @@ async def submit_for_review( ): """Linguist submits — in_progress → pending_review. Notifies reviewer by email.""" state = await lqc.submit_for_review(db, job_id, lang, current_user, http_request=http_request) + await audit_logger.log_action( + action=AuditAction.LANGUAGE_QC_SUBMIT, + description=f"Language '{lang}' submitted for review for job {job_id}", + user=current_user, + request=http_request, + resource_type="job", + resource_id=job_id, + details={"lang": lang}, + ) return LanguageQCStateResponse(lang=lang, state=state) @@ -295,6 +366,15 @@ async def open_review( ): """Reviewer opens the review — pending_review → in_review.""" state = await lqc.open_review(db, job_id, lang, current_user, http_request=http_request) + await audit_logger.log_action( + action=AuditAction.LANGUAGE_QC_OPEN_REVIEW, + description=f"Reviewer opened review for language '{lang}', job {job_id}", + user=current_user, + request=http_request, + resource_type="job", + resource_id=job_id, + details={"lang": lang}, + ) return LanguageQCStateResponse(lang=lang, state=state) @@ -314,6 +394,15 @@ async def approve_language( state = await lqc.approve_language( db, job_id, lang, current_user, http_request=http_request, notes=request.notes, ) + await audit_logger.log_action( + action=AuditAction.LANGUAGE_QC_APPROVE, + description=f"Language '{lang}' approved for job {job_id}", + user=current_user, + request=http_request, + resource_type="job", + resource_id=job_id, + details={"lang": lang, "notes": request.notes}, + ) return LanguageQCStateResponse(lang=lang, state=state) @@ -331,6 +420,15 @@ async def reject_language( state = await lqc.reject_language( db, job_id, lang, current_user, request.notes, category=request.category, http_request=http_request, ) + await audit_logger.log_action( + action=AuditAction.LANGUAGE_QC_REJECT, + description=f"Language '{lang}' rejected for job {job_id}", + user=current_user, + request=http_request, + resource_type="job", + resource_id=job_id, + details={"lang": lang, "notes": request.notes, "category": request.category}, + ) return LanguageQCStateResponse(lang=lang, state=state) @@ -343,6 +441,7 @@ async def mark_cue_reviewed( job_id: str, lang: str, request: MarkCueReviewedRequest, + http_request: Request, current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.ADMIN)), db: AsyncIOMotorDatabase = Depends(get_database), ): @@ -363,6 +462,19 @@ async def mark_cue_reviewed( state_dict = (updated_doc.get("language_qc") or {}).get(lang, {}) from ...models.job import LanguageQCState state = LanguageQCState(**state_dict) if isinstance(state_dict, dict) else LanguageQCState() + await audit_logger.log_action( + action=AuditAction.LANGUAGE_QC_MARK_CUE_REVIEWED, + description=f"Cue marked as reviewed for language '{lang}', job {job_id}", + user=current_user, + request=http_request, + resource_type="job", + resource_id=job_id, + details={ + "lang": lang, + "reviewed_cues": state.reviewed_cues if hasattr(state, "reviewed_cues") else None, + "total_cues": request.total_cues, + }, + ) return LanguageQCStateResponse(lang=lang, state=state) @@ -378,6 +490,15 @@ async def reopen_language( state = await lqc.reopen_language( db, job_id, lang, current_user, http_request=http_request, notes=request.notes, ) + await audit_logger.log_action( + action=AuditAction.LANGUAGE_QC_REOPEN, + description=f"Language '{lang}' reopened for job {job_id}", + user=current_user, + request=http_request, + resource_type="job", + resource_id=job_id, + details={"lang": lang, "notes": request.notes}, + ) return LanguageQCStateResponse(lang=lang, state=state) @@ -398,6 +519,15 @@ async def add_comment( comment = await lqc.add_comment( db, job_id, lang, current_user, request.body, http_request=http_request, ) + await audit_logger.log_action( + action=AuditAction.LANGUAGE_QC_COMMENT, + description=f"Comment added to language '{lang}' for job {job_id}", + user=current_user, + request=http_request, + resource_type="job", + resource_id=job_id, + details={"lang": lang, "comment_id": str(comment.id) if hasattr(comment, "id") else None}, + ) return comment