LinkDesk/backend/routers/reviews.py

295 lines
10 KiB
Python

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