295 lines
10 KiB
Python
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 |