HR-ATS-Portal/backend/job/app.py

506 lines
17 KiB
Python

from fastapi import APIRouter,Depends,Query
from fastapi.responses import JSONResponse
from fastapi import HTTPException
from db_setup import get_session
from job.candidate.views import CandidateScoring,FileRead,CandidateView
from job.interviews.views import Interview
from job.notes.views import Note
from job.activity.views import ActivityLog
from job.feedback.views import FeedbackView
from sqlalchemy.ext.asyncio import AsyncSession
from users.permissions import PermissionTag, require_permission
from job.job_post.views import JobPost,JobPostCreate
from job.job_post.models import JobPosts
from job.job_post.serializers import serialize_job_post
import logging
from job.job_post.plugins import PlatformAlias
from fastapi import UploadFile, File, Form
from pydantic import BaseModel
from dotenv import load_dotenv
from datetime import datetime, time, timezone
from pydantic import BaseModel
from uuid import UUID
load_dotenv()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
router = APIRouter()
class CandidateUpdate(BaseModel):
favorite: bool | None = None
rating: float | None = None
class InterviewCreate(BaseModel):
inbox_id: int
interview_date: datetime | None = None
interview_time: datetime | None = None
interview_type: str | None = None
interview_status: str | None = None
class InterviewUpdate(BaseModel):
interview_date: datetime | None = None
interview_time: datetime | None = None
interview_type: str | None = None
interview_status: str | None = None
inbox_id: int | None = None
class NoteCreate(BaseModel):
user_id: UUID
note: str
class NoteUpdate(BaseModel):
note: str | None = None
class ActivityCreate(BaseModel):
message_id: UUID | None = None
user_id: UUID | None = None
inbox_id: int | None = None
activity_type: str | None = None
activity_status: str | None = None
description: str | None = None
activity_date: datetime | None = None
activity_time: datetime | None = None
class FeedbackCreate(BaseModel):
inbox_id: int | None = None
review: str | None = None
financial_status: str | None = None
score: float | None = None
note: str | None = None
reviewed_by: UUID | None = None
class FeedbackUpdate(BaseModel):
review: str | None = None
financial_status: str | None = None
score: float | None = None
note: str | None = None
inbox_id: int | None = None
reviewed_by: UUID | None = None
@router.get("/jobs/alias")
async def get_job_alias():
try:
alias_lst=[k.name for k in PlatformAlias]
return JSONResponse(content={"data":alias_lst,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.post("/candidate/cv_upload")
async def cv_upload(
file: UploadFile = File(...),
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
session: AsyncSession = Depends(get_session),
):
try:
file_content = await file.read()
logger.info(f"Received file: {file.filename} ({len(file_content)} bytes)")
service=FileRead(session=session,filename=file.filename,file=file_content)
data=await service.read_file()
return JSONResponse(content={"data":data,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.post("/candidate/inbox-match")
async def candidate_inbox_match(
inbox_message_id: str = Query(...),
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=FileRead(session=session)
data=await service.match_inbox_cv(inbox_message_id)
return JSONResponse(content={"data":data,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.post("/job/post-job")
async def post_job(
payload: JobPostCreate,
current_user: dict = Depends(require_permission(PermissionTag.JOB_BOARD_CREATE)),
session: AsyncSession = Depends(get_session),
):
try:
service=JobPost(session=session)
data=payload.model_dump()
if data['mode']=="customScheduled":
data['due_at']=datetime.combine(
data['scheduler_date'],
data['scheduler_time'] or time(0, 0, 0),
tzinfo=timezone.utc,
).strftime("%Y-%m-%dT%H:%M:%S.000Z")
result=await service.post_job(data,current_user)
return JSONResponse(content={"data":result,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.get("/job/buffer/channels")
async def buffer_channels(
current_user: dict = Depends(require_permission(PermissionTag.JOB_BOARD_VIEW)),
session: AsyncSession = Depends(get_session),
):
try:
service=JobPost(session=session)
data=await service.list_channels()
return JSONResponse(content={"data":data,"total":len(data),"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
class InboxScoreRequest(BaseModel):
job_id: str
message_ids: list[str] # inbox_messages PK uuids, not Graph message ids
@router.post("/candidate/score")
async def score_candidates(
job_id: str = Form(...),
files: list[UploadFile] = File(...),
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
session: AsyncSession = Depends(get_session),
):
"""Score uploaded CV PDFs against a job post; persists and returns the leaderboard."""
try:
pairs=[(f.filename,await f.read()) for f in files]
service=CandidateScoring(session=session)
data=await service.score_uploads(job_id,pairs,current_user)
return JSONResponse(content={"data":data,"total":len(data),"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.post("/candidate/score_inbox")
async def score_inbox_candidates(
payload: InboxScoreRequest,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
session: AsyncSession = Depends(get_session),
):
"""Score the decoded attachments of inbox messages against a job post."""
try:
service=CandidateScoring(session=session)
data=await service.score_inbox(payload.job_id,payload.message_ids,current_user)
return JSONResponse(content={"data":data,"total":len(data),"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.get("/candidate/scored/fetch")
async def fetch_scored_candidates(
job_id: str = Query(None),
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
session: AsyncSession = Depends(get_session),
):
"""Persisted leaderboard: completed by score desc, failures last. Without job_id
returns the whole pool across jobs."""
try:
service=CandidateScoring(session=session)
data=await service.fetch_candidates(job_id)
return JSONResponse(content={"data":data,"total":len(data),"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.get("/job/fetch")
async def fetch_job_posts(
search: str | None = Query(None),
top: int | None = Query(None),
skip: int = Query(0, ge=0),
ids: str | None = Query(None),
active_only: bool = Query(True),
# Either job-board or candidate viewers may list jobs — recruiters scoring
# CVs need a job to score against (CV Import picker).
current_user: dict = Depends(
require_permission(
PermissionTag.JOB_BOARD_VIEW, PermissionTag.CANDIDATES_VIEW, require_all=False
)
),
session: AsyncSession = Depends(get_session),
):
try:
service=JobPost(session=session)
id_list=[x.strip() for x in (ids or "").split(",") if x.strip()] or None
data,total=await service.fetch_job_posts(
search=search,
top=top,
skip=skip,
ids=id_list,
active_only=active_only,
)
return JSONResponse(content={"data":data,"total":total,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.get("/candidate/fetch_by_id")
async def fetch_candidate_by_id(
candidate_id: str = Query(...),
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
session: AsyncSession = Depends(get_session),
):
try:
service=CandidateScoring(session=session)
data=await service.fetch_candidate_by_id(candidate_id)
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.get("/candidate/fetch")
async def fetch_candidate(
user_id:str=Query(None),
limit:int=Query(10),
offset:int=Query(0),
search:str=Query(None),
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
session: AsyncSession = Depends(get_session),
):
try:
service=CandidateView(session=session)
data=await service.get_candidate(user_id=user_id,limit=limit,offset=offset,search=search)
total=await service.count_candidates(user_id=user_id,search=search) if isinstance(data,list) else 1
return JSONResponse(content={"data":data,"total":total,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.patch("/candidate/update")
async def update_candidate(
user_id:str=Query(...),
payload:CandidateUpdate=...,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=CandidateView(session=session)
data=await service.update_candidate(user_id,payload.model_dump(exclude_unset=True))
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.get("/interview/fetch")
async def fetch_interview(
interview_id:str=Query(None),
inbox_id:int=Query(None),
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
session: AsyncSession = Depends(get_session),
):
try:
service=Interview(session=session)
data=await service.get_interview(interview_id=interview_id,inbox_id=inbox_id)
total=1 if isinstance(data,dict) else len(data)
return JSONResponse(content={"data":data,"total":total,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.post("/interview/create")
async def create_interview(
payload:InterviewCreate,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
session: AsyncSession = Depends(get_session),
):
try:
service=Interview(session=session)
data=await service.create_interview(payload.model_dump(exclude_unset=True))
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.patch("/interview/update")
async def update_interview(
interview_id:str=Query(...),
payload:InterviewUpdate=...,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=Interview(session=session)
data=await service.update_interview(interview_id,payload.model_dump(exclude_unset=True))
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.get("/notes/fetch")
async def fetch_notes(
note_id:str=Query(None),
user_id:str=Query(None),
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
session: AsyncSession = Depends(get_session),
):
try:
service=Note(session=session)
data=await service.get_note(note_id=note_id,user_id=user_id)
total=1 if isinstance(data,dict) else len(data)
return JSONResponse(content={"data":data,"total":total,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.post("/notes/create")
async def create_note(
payload:NoteCreate,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
session: AsyncSession = Depends(get_session),
):
try:
service=Note(session=session)
data=await service.create_note(payload.model_dump(exclude_unset=True),current_user)
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.patch("/notes/update")
async def update_note(
note_id:str=Query(...),
payload:NoteUpdate=...,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=Note(session=session)
data=await service.update_note(note_id,payload.model_dump(exclude_unset=True))
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.get("/activity/fetch")
async def fetch_activity(
activity_id:str=Query(None),
inbox_id:int=Query(None),
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
session: AsyncSession = Depends(get_session),
):
try:
service=ActivityLog(session=session)
data=await service.get_activity(activity_id=activity_id,inbox_id=inbox_id)
total=1 if isinstance(data,dict) else len(data)
return JSONResponse(content={"data":data,"total":total,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.post("/activity/create")
async def create_activity(
payload:ActivityCreate,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
session: AsyncSession = Depends(get_session),
):
try:
service=ActivityLog(session=session)
data=await service.create_activity(payload.model_dump(exclude_unset=True))
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.get("/feedback/fetch")
async def fetch_feedback(
feedback_id:str=Query(None),
inbox_id:int=Query(None),
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
session: AsyncSession = Depends(get_session),
):
try:
service=FeedbackView(session=session)
data=await service.get_feedback(feedback_id=feedback_id,inbox_id=inbox_id)
total=1 if isinstance(data,dict) else len(data)
return JSONResponse(content={"data":data,"total":total,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.post("/feedback/create")
async def create_feedback(
payload:FeedbackCreate,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
session: AsyncSession = Depends(get_session),
):
try:
service=FeedbackView(session=session)
data=await service.create_feedback(payload.model_dump(exclude_unset=True),current_user)
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))
@router.patch("/feedback/update")
async def update_feedback(
feedback_id:str=Query(...),
payload:FeedbackUpdate=...,
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
session: AsyncSession = Depends(get_session),
):
try:
service=FeedbackView(session=session)
data=await service.update_feedback(feedback_id,payload.model_dump(exclude_unset=True))
return JSONResponse(content={"data":data,"total":1,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))