Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/app/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions backend/app/models/interview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
45 changes: 45 additions & 0 deletions backend/app/routers/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
69 changes: 68 additions & 1 deletion backend/app/routers/interview.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timedelta

from fastapi import APIRouter, Depends, status
from sqlalchemy import func, select
Expand Down Expand Up @@ -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),
Expand Down
12 changes: 5 additions & 7 deletions backend/app/schemas/interview.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

from pydantic import BaseModel, model_validator

from app.exceptions.common import BadRequestError


class CustomQuestionCreate(BaseModel):
question: str
Expand Down Expand Up @@ -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

Expand Down
Loading