from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session, joinedload from sqlalchemy import and_ from typing import List, Optional from database import get_db from models.task import Task, Submission, Review, TaskStatus from models.user import User, UserRole from schemas.task import ReviewCreate, ReviewResponse, SubmissionResponse from utils.auth import get_current_user_from_token, _get_user_from_db from utils.notifications import notification_service router = APIRouter() def get_current_user( token_data: dict = Depends(get_current_user_from_token), db: Session = Depends(get_db) ): """Get current user with proper database dependency.""" return _get_user_from_db(db, token_data["user_id"]) def require_director_coordinator_or_admin( token_data: dict = Depends(get_current_user_from_token), db: Session = Depends(get_db) ): """Dependency to require director, coordinator role, or admin permission.""" current_user = _get_user_from_db(db, token_data["user_id"]) if (current_user.role not in [UserRole.DIRECTOR, UserRole.COORDINATOR] and not current_user.is_admin): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Director role, Coordinator role, or Admin permission required" ) return current_user def require_role(required_roles: list): """Create a dependency that requires specific user roles.""" def role_checker( token_data: dict = Depends(get_current_user_from_token), db: Session = Depends(get_db) ): current_user = _get_user_from_db(db, token_data["user_id"]) if current_user.role not in required_roles: raise HTTPException( status_code=403, detail="Insufficient permissions" ) return current_user return role_checker @router.get("/pending", response_model=List[SubmissionResponse]) async def get_pending_reviews( project_id: Optional[int] = Query(None, description="Filter by project ID"), skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), db: Session = Depends(get_db), current_user: User = Depends(require_director_coordinator_or_admin) ): """Get all submissions pending review. Only directors, coordinators, and users with admin permission can access.""" # Get submissions that don't have any reviews yet or have retake reviews query = db.query(Submission).options( joinedload(Submission.user), joinedload(Submission.task).joinedload(Task.project), joinedload(Submission.reviews).joinedload("reviewer") ).join(Task).filter( Submission.deleted_at.is_(None), Task.deleted_at.is_(None) ) # Filter by project if specified if project_id: query = query.filter(Task.project_id == project_id) # Only get submitted tasks query = query.filter(Task.status == "submitted") submissions = query.order_by(Submission.submitted_at.desc()).offset(skip).limit(limit).all() # Filter to only include submissions that need review pending_submissions = [] for submission in submissions: # Check if submission has any approved reviews has_approved_review = any(review.decision == "approved" for review in submission.reviews) if not has_approved_review: # Get latest review latest_review = None if submission.reviews: latest_review_obj = max(submission.reviews, key=lambda r: r.reviewed_at) latest_review = { "id": latest_review_obj.id, "submission_id": latest_review_obj.submission_id, "reviewer_id": latest_review_obj.reviewer_id, "decision": latest_review_obj.decision, "feedback": latest_review_obj.feedback, "reviewed_at": latest_review_obj.reviewed_at, "reviewer_first_name": latest_review_obj.reviewer.first_name, "reviewer_last_name": latest_review_obj.reviewer.last_name } submission_data = { "id": submission.id, "task_id": submission.task_id, "user_id": submission.user_id, "file_path": submission.file_path, "file_name": submission.file_name, "version_number": submission.version_number, "notes": submission.notes, "submitted_at": submission.submitted_at, "user_first_name": submission.user.first_name, "user_last_name": submission.user.last_name, "latest_review": latest_review } pending_submissions.append(SubmissionResponse(**submission_data)) return pending_submissions @router.post("/{submission_id}/approve", response_model=ReviewResponse) async def approve_submission( submission_id: int, review: ReviewCreate, db: Session = Depends(get_db), current_user: User = Depends(require_director_coordinator_or_admin) ): """Approve a submission. Only directors, coordinators, and users with admin permission can approve.""" submission = db.query(Submission).options( joinedload(Submission.task) ).filter( Submission.id == submission_id, Submission.deleted_at.is_(None) ).first() if not submission: raise HTTPException(status_code=404, detail="Submission not found") # Check if submission is in submitted state if submission.task.status != "submitted": raise HTTPException(status_code=400, detail="Submission is not in submitted state") # Force decision to approved review.decision = "approved" # Create review record db_review = Review( submission_id=submission_id, reviewer_id=current_user.id, decision=review.decision, feedback=review.feedback ) db.add(db_review) # Update task status to approved submission.task.status = "approved" db.commit() db.refresh(db_review) # Send notification to artist notification_service.notify_submission_reviewed(db, submission, db_review, current_user) # Load reviewer information for response db_review = db.query(Review).options( joinedload(Review.reviewer) ).filter(Review.id == db_review.id).first() review_data = { "id": db_review.id, "submission_id": db_review.submission_id, "reviewer_id": db_review.reviewer_id, "decision": db_review.decision, "feedback": db_review.feedback, "reviewed_at": db_review.reviewed_at, "reviewer_first_name": db_review.reviewer.first_name, "reviewer_last_name": db_review.reviewer.last_name } return ReviewResponse(**review_data) @router.post("/{submission_id}/retake", response_model=ReviewResponse) async def request_retake( submission_id: int, review: ReviewCreate, db: Session = Depends(get_db), current_user: User = Depends(require_director_coordinator_or_admin) ): """Request a retake for a submission. Only directors, coordinators, and users with admin permission can request retakes.""" submission = db.query(Submission).options( joinedload(Submission.task) ).filter( Submission.id == submission_id, Submission.deleted_at.is_(None) ).first() if not submission: raise HTTPException(status_code=404, detail="Submission not found") # Check if submission is in submitted state if submission.task.status != "submitted": raise HTTPException(status_code=400, detail="Submission is not in submitted state") # Force decision to retake and require feedback review.decision = "retake" if not review.feedback or review.feedback.strip() == "": raise HTTPException(status_code=400, detail="Feedback is required when requesting a retake") # Create review record db_review = Review( submission_id=submission_id, reviewer_id=current_user.id, decision=review.decision, feedback=review.feedback ) db.add(db_review) # Update task status to retake submission.task.status = "retake" db.commit() db.refresh(db_review) # Send notification to artist notification_service.notify_submission_reviewed(db, submission, db_review, current_user) # Load reviewer information for response db_review = db.query(Review).options( joinedload(Review.reviewer) ).filter(Review.id == db_review.id).first() review_data = { "id": db_review.id, "submission_id": db_review.submission_id, "reviewer_id": db_review.reviewer_id, "decision": db_review.decision, "feedback": db_review.feedback, "reviewed_at": db_review.reviewed_at, "reviewer_first_name": db_review.reviewer.first_name, "reviewer_last_name": db_review.reviewer.last_name } return ReviewResponse(**review_data) @router.get("/{submission_id}/reviews", response_model=List[ReviewResponse]) async def get_submission_reviews( submission_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get all reviews for a submission.""" submission = db.query(Submission).options( joinedload(Submission.task) ).filter( Submission.id == submission_id, Submission.deleted_at.is_(None) ).first() if not submission: raise HTTPException(status_code=404, detail="Submission not found") # Artists can only view reviews for their own submissions if current_user.role == UserRole.ARTIST and submission.user_id != current_user.id: raise HTTPException(status_code=403, detail="Not authorized to view reviews for this submission") reviews = db.query(Review).options( joinedload(Review.reviewer) ).filter( Review.submission_id == submission_id, Review.deleted_at.is_(None) ).order_by(Review.reviewed_at.desc()).all() result = [] for review in reviews: review_data = { "id": review.id, "submission_id": review.submission_id, "reviewer_id": review.reviewer_id, "decision": review.decision, "feedback": review.feedback, "reviewed_at": review.reviewed_at, "reviewer_first_name": review.reviewer.first_name, "reviewer_last_name": review.reviewer.last_name } result.append(ReviewResponse(**review_data)) return result