diff --git a/backend/app/models/base.py b/backend/app/models/base.py index b205280..80445ec 100644 --- a/backend/app/models/base.py +++ b/backend/app/models/base.py @@ -8,10 +8,10 @@ class TimestampMixin: created_at: Mapped[datetime] = mapped_column( - DateTime, default=datetime.utcnow, server_default=func.now() + DateTime(timezone=True), default=datetime.utcnow, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( - DateTime, + DateTime(timezone=True), default=datetime.utcnow, server_default=func.now(), onupdate=datetime.utcnow, diff --git a/backend/app/models/interview.py b/backend/app/models/interview.py index f780e33..c114b1c 100644 --- a/backend/app/models/interview.py +++ b/backend/app/models/interview.py @@ -24,9 +24,9 @@ class CustomInterview(BaseTable): description: Mapped[str] = mapped_column(Text, nullable=False) position: Mapped[str] = mapped_column(Text, nullable=False) experience: Mapped[str] = mapped_column(String(10), nullable=False) - submission_deadline: Mapped[datetime] = mapped_column(DateTime, nullable=False) - start_time: Mapped[datetime] = mapped_column(DateTime, nullable=False) - end_time: Mapped[datetime] = mapped_column(DateTime, nullable=False) + submission_deadline: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) + start_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) + end_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) duration: Mapped[int] = mapped_column(Integer, default=60) dsa_score: Mapped[int | None] = mapped_column(Integer, nullable=False) dev_score: Mapped[int | None] = mapped_column(Integer, nullable=False) diff --git a/backend/app/routers/application.py b/backend/app/routers/application.py index 2554f7b..108faed 100644 --- a/backend/app/routers/application.py +++ b/backend/app/routers/application.py @@ -112,3 +112,48 @@ async def apply_for_interview( logger.info("Application created successfully: %d", application.id) return ApplicationResponse.model_validate(application) + + +@router.patch("/{application_id}/shortlist", response_model=ApplicationResponse) +async def shortlist_application( + application_id: int, + db: AsyncSession = Depends(get_db), + org: Organization = Depends(is_organization), +) -> ApplicationResponse: + """ + Approve or reject (toggle shortlisting_decision) for an application. + Only the org that owns the interview may do this. + """ + logger.info( + "Shortlist toggle request for application: %d by org: %d", + application_id, + org.id, + ) + + app_result = await db.execute( + select(Application).where(Application.id == application_id) + ) + application = app_result.scalar_one_or_none() + + if not application: + raise NotFoundError("Application not found") + + # Verify the org owns the interview this application belongs to + interview_result = await db.execute( + select(CustomInterview).where(CustomInterview.id == application.interview_id) + ) + interview = interview_result.scalar_one_or_none() + + if not interview or interview.org_id != org.id: + raise ForbiddenError("You cannot modify this application") + + application.shortlisting_decision = not application.shortlisting_decision + await db.commit() + await db.refresh(application) + + logger.info( + "Application %d shortlisting_decision set to %s", + application_id, + application.shortlisting_decision, + ) + return ApplicationResponse.model_validate(application) diff --git a/backend/app/routers/interview.py b/backend/app/routers/interview.py index 6dc2466..5916fcf 100644 --- a/backend/app/routers/interview.py +++ b/backend/app/routers/interview.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timedelta from fastapi import APIRouter, Depends, status from sqlalchemy import func, select @@ -135,7 +135,74 @@ async def get_applied_interviews( return applied_interviews + +@router.post( + "/seed-test", + response_model=CustomInterviewResponse, + status_code=status.HTTP_201_CREATED, +) +async def seed_test_interview( + db: AsyncSession = Depends(get_db), + org: Organization = Depends(is_organization), +) -> CustomInterviewResponse: + """ + Create a fully-populated test interview with future dates. + Useful for manual QA without going through the full wizard. + """ + logger.info("Seed test interview for org: %d", org.id) + + now = datetime.utcnow() + + interview = CustomInterview( + org_id=org.id, + description=( + "This is a seeded test interview for QA purposes. " + "It covers Python backend development and data structures." + ), + position="Software Engineer (Test)", + experience="Mid", + submission_deadline=now + timedelta(days=1), + start_time=now + timedelta(days=2), + end_time=now + timedelta(days=5), + duration=30, + dsa_score=50, + dev_score=50, + resume_shortlist_score=0, + ask_questions_on_resume=False, + ) + + for q in [ + CustomQuestion( + question="Explain the difference between a process and a thread.", + expected_answer="A process has its own memory space; threads share memory within a process.", + ), + CustomQuestion( + question="What is the time complexity of binary search?", + expected_answer="O(log n)", + ), + CustomQuestion( + question="Describe how you would design a REST API for a blog platform.", + expected_answer="Resources: /posts, /users, /comments. Use GET/POST/PUT/DELETE verbs, pagination, auth via JWT.", + ), + ]: + interview.questions.append(q) + + for t in [ + DsaTopic(topic="Arrays", difficulty="easy"), + DsaTopic(topic="Binary Search", difficulty="medium"), + ]: + interview.dsa_topics.append(t) + + db.add(interview) + await db.commit() + await db.refresh(interview, attribute_names=["questions", "dsa_topics"]) + + logger.info("Test interview created: %d for org: %d", interview.id, org.id) + return CustomInterviewResponse.model_validate(interview) + + @router.get("/{interview_id}", response_model=CustomInterviewResponse) + async def get_interview( interview_id: int, db: AsyncSession = Depends(get_db), diff --git a/backend/app/schemas/interview.py b/backend/app/schemas/interview.py index 1c5c9bb..a178c9c 100644 --- a/backend/app/schemas/interview.py +++ b/backend/app/schemas/interview.py @@ -2,8 +2,6 @@ from pydantic import BaseModel, model_validator -from app.exceptions.common import BadRequestError - class CustomQuestionCreate(BaseModel): question: str @@ -37,20 +35,20 @@ def validate_times_and_scores(self) -> "CustomInterviewCreate": now = datetime.now(tz) if self.start_time <= now: - raise BadRequestError("start_time must be in the future") + raise ValueError("start_time must be in the future") if self.end_time <= now: - raise BadRequestError("end_time must be in the future") + raise ValueError("end_time must be in the future") if self.submission_deadline <= now: - raise BadRequestError("submission_deadline must be in the future") + raise ValueError("submission_deadline must be in the future") if self.end_time <= self.start_time: - raise BadRequestError("end_time must be after start_time") + raise ValueError("end_time must be after start_time") if self.dsa_score is not None or self.dev_score is not None: dsa = self.dsa_score or 0 dev = self.dev_score or 0 if dsa + dev != 100: - raise BadRequestError("The sum of dsa_score and dev_score must be exactly 100") + raise ValueError("The sum of dsa_score and dev_score must be exactly 100") return self