Hirely-Backend / backend /api /endpoints /interviews.py
NaikPranav11's picture
Initial clean deployment
1207440
Raw
History Blame Contribute Delete
14.4 kB
"""
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"])
@router.get("/", response_model=List[InterviewResponse])
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
@router.get("/stats")
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
}
@router.get("/scheduled-today")
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]
@router.get("/{interview_id}", response_model=InterviewResponse)
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
@router.post("/", response_model=InterviewResponse)
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
@router.put("/{interview_id}", response_model=InterviewResponse)
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
@router.delete("/{interview_id}", response_model=BaseResponse)
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"}
@router.post("/{interview_id}/start", response_model=InterviewResponse)
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
@router.post("/{interview_id}/complete", response_model=InterviewResponse)
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
@router.post("/{interview_id}/score")
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"}