Spaces:
Runtime error
Runtime error
| """ | |
| Interviews API endpoints | |
| """ | |
| from typing import List, Optional | |
| from fastapi import APIRouter, Depends, HTTPException, status, Query | |
| from sqlalchemy.orm import Session, joinedload | |
| from sqlalchemy import or_, and_ | |
| from models import get_db, Interview, User, Candidate, Job | |
| from api.auth import get_current_active_user, require_recruiter, require_interviewer | |
| from api.schemas import ( | |
| InterviewCreate, InterviewUpdate, InterviewResponse, BaseResponse | |
| ) | |
| from datetime import datetime | |
| router = APIRouter(prefix="/interviews", tags=["Interviews"]) | |
| async def get_interviews( | |
| skip: int = Query(0, ge=0), | |
| limit: int = Query(10, ge=1, le=10000), | |
| status: Optional[str] = Query(None), | |
| interview_type: Optional[str] = Query(None), | |
| candidate_id: Optional[int] = Query(None), | |
| job_id: Optional[int] = Query(None), | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| """ | |
| Get all interviews with optional filtering | |
| """ | |
| query = db.query(Interview).options( | |
| joinedload(Interview.candidate), | |
| joinedload(Interview.job), | |
| joinedload(Interview.interviewer_user) | |
| ) | |
| # Apply filters | |
| if status: | |
| query = query.filter(Interview.status == status.lower()) | |
| if interview_type: | |
| query = query.filter(Interview.interview_type == interview_type.lower()) | |
| if candidate_id: | |
| query = query.filter(Interview.candidate_id == candidate_id) | |
| if job_id: | |
| query = query.filter(Interview.job_id == job_id) | |
| # Order by scheduled date | |
| query = query.order_by(Interview.scheduled_at.desc()) | |
| interviews = query.offset(skip).limit(limit).all() | |
| return interviews | |
| async def get_interview_stats( | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| """ | |
| Get interview statistics | |
| """ | |
| total_interviews = db.query(Interview).count() | |
| scheduled = db.query(Interview).filter(Interview.status == "scheduled").count() | |
| completed = db.query(Interview).filter(Interview.status == "completed").count() | |
| ai_interviews = db.query(Interview).filter(Interview.interview_type == "ai_interview").count() | |
| live_interviews = db.query(Interview).filter(Interview.interview_type == "live_interview").count() | |
| return { | |
| "total_interviews": total_interviews, | |
| "scheduled": scheduled, | |
| "completed": completed, | |
| "ai_interviews": ai_interviews, | |
| "live_interviews": live_interviews | |
| } | |
| async def get_scheduled_today( | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| """ | |
| Get interviews scheduled for today | |
| """ | |
| from datetime import date | |
| today = date.today() | |
| interviews = db.query(Interview).filter( | |
| and_( | |
| Interview.status == "scheduled", | |
| Interview.scheduled_at >= today, | |
| Interview.scheduled_at < today.replace(day=today.day + 1) | |
| ) | |
| ).order_by(Interview.scheduled_at).all() | |
| return [interview.to_dict() for interview in interviews] | |
| async def get_interview( | |
| interview_id: int, | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(get_current_active_user) | |
| ): | |
| """ | |
| Get a specific interview by ID | |
| """ | |
| interview = db.query(Interview).options( | |
| joinedload(Interview.candidate), | |
| joinedload(Interview.job), | |
| joinedload(Interview.interviewer_user) | |
| ).filter(Interview.id == interview_id).first() | |
| if not interview: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Interview not found" | |
| ) | |
| return interview | |
| async def create_interview( | |
| interview_data: InterviewCreate, | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(require_recruiter) | |
| ): | |
| """ | |
| Schedule a new interview | |
| """ | |
| # Validate candidate exists | |
| candidate = db.query(Candidate).filter(Candidate.id == interview_data.candidate_id).first() | |
| if not candidate: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Candidate not found" | |
| ) | |
| # Validate job exists | |
| job = db.query(Job).filter(Job.id == interview_data.job_id).first() | |
| if not job: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Job not found" | |
| ) | |
| # Validate interviewer exists (if provided) | |
| if interview_data.interviewer_id: | |
| interviewer = db.query(User).filter(User.id == interview_data.interviewer_id).first() | |
| if not interviewer: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Interviewer not found" | |
| ) | |
| # Create interview | |
| interview_dict = interview_data.dict() | |
| interview_dict["status"] = "scheduled" | |
| # For AI interviews, ensure no interviewer is assigned | |
| if interview_data.interview_type == "ai_interview": | |
| interview_dict["interviewer_id"] = None | |
| interview = Interview(**interview_dict) | |
| db.add(interview) | |
| db.commit() | |
| db.refresh(interview) | |
| # Update candidate status | |
| from models.utils import update_candidate_status | |
| update_candidate_status(db, interview_data.candidate_id, "interview_scheduled") | |
| # Send email notifications (don't block on failure) | |
| try: | |
| from modules.notification_service import send_interview_notifications | |
| notification_result = send_interview_notifications(db, interview, 'created') | |
| if notification_result['errors']: | |
| # Log errors but don't fail the request | |
| print(f"Email notification errors: {notification_result['errors']}") | |
| except ImportError as e: | |
| print(f"Failed to import notification service: {str(e)}") | |
| except Exception as e: | |
| # Log error but don't block interview creation | |
| print(f"Failed to send interview notifications: {str(e)}") | |
| import traceback | |
| traceback.print_exc() | |
| return interview | |
| async def update_interview( | |
| interview_id: int, | |
| interview_data: InterviewUpdate, | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(require_recruiter) | |
| ): | |
| """ | |
| Update an interview | |
| """ | |
| interview = db.query(Interview).filter(Interview.id == interview_id).first() | |
| if not interview: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Interview not found" | |
| ) | |
| # Track if scheduled_at changed for rescheduling notifications | |
| scheduled_at_changed = False | |
| if interview_data.scheduled_at and interview_data.scheduled_at != interview.scheduled_at: | |
| scheduled_at_changed = True | |
| # Validate status transition if status is being updated | |
| update_data = interview_data.dict(exclude_unset=True) | |
| if 'status' in update_data and update_data['status'] != interview.status: | |
| from models.utils import validate_interview_status_transition | |
| is_valid, error_message = validate_interview_status_transition( | |
| interview.status, update_data['status'] | |
| ) | |
| if not is_valid: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail=error_message | |
| ) | |
| # Record timestamps for status changes | |
| new_status = update_data['status'].lower() | |
| if new_status == 'completed' and not interview.completed_at: | |
| interview.completed_at = datetime.utcnow() | |
| elif new_status == 'cancelled' and not interview.cancelled_at: | |
| interview.cancelled_at = datetime.utcnow() | |
| # Update fields | |
| for field, value in update_data.items(): | |
| if hasattr(interview, field): | |
| setattr(interview, field, value) | |
| # Update the updated_at timestamp | |
| interview.updated_at = datetime.utcnow() | |
| db.commit() | |
| db.refresh(interview) | |
| # Send rescheduling notifications if date/time changed | |
| if scheduled_at_changed: | |
| try: | |
| from modules.notification_service import send_interview_notifications | |
| notification_result = send_interview_notifications(db, interview, 'updated') | |
| if notification_result['errors']: | |
| print(f"Email notification errors: {notification_result['errors']}") | |
| except Exception as e: | |
| print(f"Failed to send rescheduling notifications: {str(e)}") | |
| return interview | |
| async def delete_interview( | |
| interview_id: int, | |
| cancellation_reason: Optional[str] = None, | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(require_recruiter) | |
| ): | |
| """ | |
| Cancel/delete an interview | |
| """ | |
| interview = db.query(Interview).filter(Interview.id == interview_id).first() | |
| if not interview: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Interview not found" | |
| ) | |
| # Set status to cancelled and record timestamp | |
| interview.status = "cancelled" | |
| interview.cancelled_at = datetime.utcnow() | |
| interview.cancellation_reason = cancellation_reason or "Cancelled by recruiter" | |
| db.commit() | |
| # Send cancellation notifications | |
| try: | |
| from modules.notification_service import send_interview_notifications | |
| notification_result = send_interview_notifications(db, interview, 'cancelled') | |
| if notification_result['errors']: | |
| print(f"Email notification errors: {notification_result['errors']}") | |
| except Exception as e: | |
| print(f"Failed to send cancellation notifications: {str(e)}") | |
| return {"success": True, "message": "Interview cancelled successfully"} | |
| async def start_interview( | |
| interview_id: int, | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(require_interviewer) | |
| ): | |
| """ | |
| Start an interview | |
| """ | |
| interview = db.query(Interview).filter(Interview.id == interview_id).first() | |
| if not interview: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Interview not found" | |
| ) | |
| # Validate status transition | |
| from models.utils import validate_interview_status_transition | |
| is_valid, error_message = validate_interview_status_transition( | |
| interview.status, 'in_progress' | |
| ) | |
| if not is_valid: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail=error_message | |
| ) | |
| interview.status = "in_progress" | |
| interview.started_at = datetime.utcnow() | |
| db.commit() | |
| db.refresh(interview) | |
| return interview | |
| async def complete_interview( | |
| interview_id: int, | |
| feedback: Optional[str] = None, | |
| rating: Optional[int] = None, | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(require_interviewer) | |
| ): | |
| """ | |
| Complete an interview | |
| """ | |
| interview = db.query(Interview).filter(Interview.id == interview_id).first() | |
| if not interview: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Interview not found" | |
| ) | |
| # Validate status transition | |
| from models.utils import validate_interview_status_transition | |
| is_valid, error_message = validate_interview_status_transition( | |
| interview.status, 'completed' | |
| ) | |
| if not is_valid: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail=error_message | |
| ) | |
| interview.status = "completed" | |
| interview.completed_at = datetime.utcnow() | |
| if feedback: | |
| interview.interviewer_feedback = feedback | |
| if rating: | |
| interview.interviewer_rating = rating | |
| db.commit() | |
| db.refresh(interview) | |
| # Update candidate status | |
| from models.utils import update_candidate_status | |
| update_candidate_status(db, interview.candidate_id, "interview_completed") | |
| return interview | |
| async def update_interview_scores( | |
| interview_id: int, | |
| scores: dict, | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(require_interviewer) | |
| ): | |
| """ | |
| Update interview scores (for AI interviews) | |
| """ | |
| interview = db.query(Interview).filter(Interview.id == interview_id).first() | |
| if not interview: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Interview not found" | |
| ) | |
| # Update scores | |
| if "overall_score" in scores: | |
| interview.overall_score = scores["overall_score"] | |
| if "technical_score" in scores: | |
| interview.technical_score = scores["technical_score"] | |
| if "communication_score" in scores: | |
| interview.communication_score = scores["communication_score"] | |
| if "problem_solving_score" in scores: | |
| interview.problem_solving_score = scores["problem_solving_score"] | |
| if "cultural_fit_score" in scores: | |
| interview.cultural_fit_score = scores["cultural_fit_score"] | |
| if "clarity_score" in scores: | |
| interview.clarity_score = scores["clarity_score"] | |
| if "relevance_score" in scores: | |
| interview.relevance_score = scores["relevance_score"] | |
| if "conciseness_score" in scores: | |
| interview.conciseness_score = scores["conciseness_score"] | |
| db.commit() | |
| # Also update candidate overall score | |
| candidate = db.query(Candidate).filter(Candidate.id == interview.candidate_id).first() | |
| if candidate and interview.overall_score: | |
| candidate.overall_score = interview.overall_score | |
| candidate.technical_score = interview.technical_score | |
| candidate.communication_score = interview.communication_score | |
| candidate.cultural_fit_score = interview.cultural_fit_score | |
| db.commit() | |
| return {"success": True, "message": "Scores updated successfully"} |