From 87135f5c605d7a2430e11ceb40ee64317e5e6d7d Mon Sep 17 00:00:00 2001 From: jeez26 Date: Mon, 8 Sep 2025 16:08:25 +0300 Subject: [PATCH] Added reports router --- .../interview_reports_repository.py | 88 ++++++++++++++ app/routers/interview_reports_router.py | 113 ++++++++++++++++++ app/services/interview_reports_service.py | 87 ++++++++++++++ main.py | 3 +- pyproject.toml | 1 + 5 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 app/repositories/interview_reports_repository.py create mode 100644 app/routers/interview_reports_router.py create mode 100644 app/services/interview_reports_service.py diff --git a/app/repositories/interview_reports_repository.py b/app/repositories/interview_reports_repository.py new file mode 100644 index 0000000..57a63b6 --- /dev/null +++ b/app/repositories/interview_reports_repository.py @@ -0,0 +1,88 @@ +from datetime import datetime +from typing import Annotated + +from fastapi import Depends +from sqlalchemy import select, update +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_session +from app.models.interview_report import InterviewReport +from app.models.interview import InterviewSession +from app.models.resume import Resume +from app.models.vacancy import Vacancy +from app.repositories.base_repository import BaseRepository + + +class InterviewReportRepository(BaseRepository[InterviewReport]): + def __init__(self, session: Annotated[AsyncSession, Depends(get_session)]): + super().__init__(InterviewReport, session) + + async def get_by_session_id(self, session_id: int) -> InterviewReport | None: + """Получить отчёт по ID сессии интервью""" + statement = select(InterviewReport).where( + InterviewReport.interview_session_id == session_id + ) + result = await self._session.execute(statement) + return result.scalar_one_or_none() + + async def update_scores( + self, + report_id: int, + scores: dict, + ) -> bool: + """Обновить оценки в отчёте""" + try: + await self._session.execute( + update(InterviewReport) + .where(InterviewReport.id == report_id) + .values( + **scores, + updated_at=datetime.utcnow(), + ) + ) + await self._session.commit() + return True + except Exception: + await self._session.rollback() + return False + + async def update_pdf_url(self, report_id: int, pdf_url: str) -> bool: + """Обновить ссылку на PDF отчёта""" + try: + await self._session.execute( + update(InterviewReport) + .where(InterviewReport.id == report_id) + .values(pdf_report_url=pdf_url, updated_at=datetime.utcnow()) + ) + await self._session.commit() + return True + except Exception: + await self._session.rollback() + return False + + async def get_by_vacancy_id(self, vacancy_id: int) -> list[InterviewReport]: + """Получить все отчёты по вакансии""" + statement = ( + select(InterviewReport) + .join(InterviewSession, InterviewSession.id == InterviewReport.interview_session_id) + .join(Resume, Resume.id == InterviewSession.resume_id) + .join(Vacancy, Vacancy.id == Resume.vacancy_id) + .where(Vacancy.id == vacancy_id) + .order_by(InterviewReport.overall_score.desc()) + ) + result = await self._session.execute(statement) + return result.scalars().all() + + async def update_notes(self, report_id: int, notes: str) -> bool: + """Обновить заметки интервьюера""" + try: + await self._session.execute( + update(InterviewReport) + .where(InterviewReport.id == report_id) + .values(interviewer_notes=notes, updated_at=datetime.utcnow()) + ) + await self._session.commit() + return True + except Exception: + await self._session.rollback() + return False diff --git a/app/routers/interview_reports_router.py b/app/routers/interview_reports_router.py new file mode 100644 index 0000000..15c6405 --- /dev/null +++ b/app/routers/interview_reports_router.py @@ -0,0 +1,113 @@ +from fastapi import APIRouter, Depends, HTTPException, Request +from typing import List + +from app.core.session_middleware import get_current_session +from app.models.session import Session +from app.models.interview_report import InterviewReport +from app.services.interview_reports_service import InterviewReportService + +router = APIRouter(prefix="/interview-reports", tags=["interview-reports"]) + + +@router.get("/vacancy/{vacancy_id}", response_model=List[InterviewReport]) +async def get_reports_by_vacancy( + vacancy_id: int, + current_session: Session = Depends(get_current_session), + report_service: InterviewReportService = Depends(InterviewReportService), +): + """Получить все отчёты по вакансии""" + if not current_session: + raise HTTPException(status_code=401, detail="No active session") + + reports = await report_service.get_reports_by_vacancy(vacancy_id) + return reports + + +@router.get("/session/{session_id}", response_model=InterviewReport) +async def get_report_by_session( + session_id: int, + current_session: Session = Depends(get_current_session), + report_service: InterviewReportService = Depends(InterviewReportService), +): + """Получить отчёт по сессии интервью""" + if not current_session: + raise HTTPException(status_code=401, detail="No active session") + + report = await report_service.get_report_by_session(session_id) + if not report: + raise HTTPException(status_code=404, detail="Report not found") + + return report + + +@router.patch("/{report_id}/scores") +async def update_report_scores( + report_id: int, + scores: dict, + current_session: Session = Depends(get_current_session), + report_service: InterviewReportService = Depends(InterviewReportService), +): + """Обновить оценки отчёта""" + if not current_session: + raise HTTPException(status_code=401, detail="No active session") + + success = await report_service.update_report_scores(report_id, scores) + if not success: + raise HTTPException(status_code=500, detail="Failed to update report scores") + + return {"message": "Report scores updated successfully"} + + +@router.patch("/{report_id}/notes") +async def update_report_notes( + report_id: int, + notes: str, + current_session: Session = Depends(get_current_session), + report_service: InterviewReportService = Depends(InterviewReportService), +): + """Обновить заметки интервьюера""" + if not current_session: + raise HTTPException(status_code=401, detail="No active session") + + success = await report_service.update_interviewer_notes(report_id, notes) + if not success: + raise HTTPException( + status_code=500, detail="Failed to update interviewer notes" + ) + + return {"message": "Interviewer notes updated successfully"} + + +@router.patch("/{report_id}/pdf") +async def update_report_pdf( + report_id: int, + pdf_url: str, + current_session: Session = Depends(get_current_session), + report_service: InterviewReportService = Depends(InterviewReportService), +): + """Обновить PDF отчёта""" + if not current_session: + raise HTTPException(status_code=401, detail="No active session") + + success = await report_service.update_pdf_url(report_id, pdf_url) + if not success: + raise HTTPException(status_code=500, detail="Failed to update PDF URL") + + return {"message": "PDF URL updated successfully"} + + +@router.post("/create") +async def create_report( + report_data: dict, + current_session: Session = Depends(get_current_session), + report_service: InterviewReportService = Depends(InterviewReportService), +): + """Создать новый отчёт интервью""" + if not current_session: + raise HTTPException(status_code=401, detail="No active session") + + report = await report_service.create_report(**report_data) + if not report: + raise HTTPException(status_code=500, detail="Failed to create report") + + return report diff --git a/app/services/interview_reports_service.py b/app/services/interview_reports_service.py new file mode 100644 index 0000000..0eea84c --- /dev/null +++ b/app/services/interview_reports_service.py @@ -0,0 +1,87 @@ +from datetime import datetime +from typing import Annotated + +from fastapi import Depends + +from app.models.interview_report import InterviewReport +from app.repositories.interview_reports_repository import InterviewReportRepository + + +class InterviewReportService: + def __init__( + self, + report_repo: Annotated[InterviewReportRepository, Depends(InterviewReportRepository)], + ): + self.report_repo = report_repo + + async def get_report_by_session(self, session_id: int) -> InterviewReport | None: + """Получить отчёт по ID сессии""" + return await self.report_repo.get_by_session_id(session_id) + + async def get_reports_by_vacancy(self, vacancy_id: int) -> list[InterviewReport]: + """Получить все отчёты по вакансии""" + return await self.report_repo.get_by_vacancy_id(vacancy_id) + + async def update_report_scores( + self, report_id: int, scores: dict + ) -> bool: + """ + Обновить оценки отчёта. + Пример scores: + { + "technical_skills_score": 8, + "communication_score": 7, + "overall_score": 8 + } + """ + return await self.report_repo.update_scores(report_id, scores) + + async def update_pdf_url(self, report_id: int, pdf_url: str) -> bool: + """Обновить ссылку на PDF отчёта""" + return await self.report_repo.update_pdf_url(report_id, pdf_url) + + async def update_interviewer_notes(self, report_id: int, notes: str) -> bool: + """Обновить заметки интервьюера""" + return await self.report_repo.update_notes(report_id, notes) + + async def create_report( + self, + interview_session_id: int, + technical_skills_score: int, + experience_relevance_score: int, + communication_score: int, + problem_solving_score: int, + cultural_fit_score: int, + overall_score: int, + recommendation: str, + strengths: dict | None = None, + weaknesses: dict | None = None, + red_flags: dict | None = None, + next_steps: str | None = None, + interviewer_notes: str | None = None, + pdf_report_url: str | None = None, + ) -> InterviewReport | None: + """Создать новый отчёт для сессии""" + try: + report_data = { + "interview_session_id": interview_session_id, + "technical_skills_score": technical_skills_score, + "experience_relevance_score": experience_relevance_score, + "communication_score": communication_score, + "problem_solving_score": problem_solving_score, + "cultural_fit_score": cultural_fit_score, + "overall_score": overall_score, + "recommendation": recommendation, + "strengths": strengths or {}, + "weaknesses": weaknesses or {}, + "red_flags": red_flags or {}, + "next_steps": next_steps, + "interviewer_notes": interviewer_notes, + "pdf_report_url": pdf_report_url, + "created_at": datetime.utcnow(), + "updated_at": datetime.utcnow(), + } + return await self.report_repo.create(report_data) + except Exception as e: + print(f"Error creating interview report: {str(e)}") + return None diff --git a/main.py b/main.py index 759b194..7b03ce1 100644 --- a/main.py +++ b/main.py @@ -9,6 +9,7 @@ from app.routers.admin_router import router as admin_router from app.routers.analysis_router import router as analysis_router from app.routers.interview_router import router as interview_router from app.routers.session_router import router as session_router +from app.routers.interview_reports_router import router as interview_report_router @asynccontextmanager @@ -56,7 +57,7 @@ app.include_router(session_router, prefix="/api/v1") app.include_router(interview_router, prefix="/api/v1") app.include_router(analysis_router, prefix="/api/v1") app.include_router(admin_router, prefix="/api/v1") - +app.include_router(interview_report_router, prefix="/api/v1") @app.get("/") async def root(): diff --git a/pyproject.toml b/pyproject.toml index d705d25..d9dd49c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "yandex-speechkit>=1.5.0", "pdfkit>=1.0.0", "jinja2>=3.1.6", + "greenlet>=3.2.4", ] [build-system]