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
235 changes: 232 additions & 3 deletions wacruit/src/apps/recruiting/repositories.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from typing import Sequence

from alembic.util.messaging import status
from fastapi import Depends
from sqlalchemy import case
from sqlalchemy import func
from sqlalchemy import select
from sqlalchemy.orm import contains_eager
from sqlalchemy.orm import Session
from sqlalchemy.orm import contains_eager

from wacruit.src.apps.common.enums import CodeSubmissionResultStatus
from wacruit.src.apps.common.enums import RecruitingApplicationStatus
from wacruit.src.apps.common.enums import RecruitingType
from wacruit.src.apps.problem.models import CodeSubmission
from wacruit.src.apps.problem.models import CodeSubmissionResult
from wacruit.src.apps.problem.models import Problem
from wacruit.src.apps.recruiting.exceptions import RecruitingAlreadyAppliedException
from wacruit.src.apps.recruiting.exceptions import RecruitingClosedException
Expand All @@ -18,8 +20,11 @@
from wacruit.src.apps.recruiting.models import Recruiting
from wacruit.src.apps.recruiting.models import RecruitingApplication
from wacruit.src.apps.recruiting_info.models import RecruitingInfo
from wacruit.src.database.connection import get_db_session
from wacruit.src.apps.resume.models import ResumeQuestion
from wacruit.src.apps.resume.models import ResumeSubmission
from wacruit.src.apps.user.models import User
from wacruit.src.database.connection import Transaction
from wacruit.src.database.connection import get_db_session


class RecruitingRepository:
Expand Down Expand Up @@ -182,3 +187,227 @@ def update_recruiting_info(self, recruiting_info: RecruitingInfo) -> RecruitingI
self.session.merge(recruiting_info)
self.session.flush()
return recruiting_info

def get_submissions_by_recruiting_id(
self, recruiting_id: int, limit: int, offset: int
):
Comment on lines +191 to +193

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

반환 타입 주석이 누락되었습니다.

메서드의 반환 타입을 명시하면 타입 체킹과 코드 가독성이 향상됩니다.

📝 제안하는 타입 주석 추가
-    def get_submissions_by_recruiting_id(
-        self, recruiting_id: int, limit: int, offset: int
-    ):
+    def get_submissions_by_recruiting_id(
+        self, recruiting_id: int, limit: int, offset: int
+    ) -> Sequence[dict]:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wacruit/src/apps/recruiting/repositories.py` around lines 191 - 193, Add an
explicit return type annotation to the method get_submissions_by_recruiting_id
to improve type checking and readability; update its signature to include an
appropriate return type such as list[Submission] or Sequence[Submission] (or the
project's ORM type e.g., QuerySet[Submission]) and import the needed typing
symbol (List/Sequence) and Submission model/type if not already imported, making
sure the annotated type matches the actual value returned by the method.

question_ids = (
self.session.execute(
select(ResumeQuestion.id)
.where(ResumeQuestion.recruiting_id == recruiting_id)
.order_by(ResumeQuestion.question_num)
)
.scalars()
.all()
)
q_ids = list(question_ids) + [-1, -1, -1]
q1_id, q2_id, q3_id = q_ids[:3]

problem_ids = (
self.session.execute(
select(Problem.id)
.where(Problem.recruiting_id == recruiting_id)
.order_by(Problem.num)
)
.scalars()
.all()
)
p_ids = list(problem_ids) + [-1, -1, -1]
p1_id, p2_id, p3_id = p_ids[:3]

resume_max_time = (
select(
ResumeSubmission.user_id,
ResumeSubmission.question_id,
func.max(ResumeSubmission.created_at).label("max_time"),
)
.where(ResumeSubmission.question_id.in_([q1_id, q2_id, q3_id]))
.group_by(ResumeSubmission.user_id, ResumeSubmission.question_id)
.subquery("resume_max_time")
)

resume_latest = (
select(
ResumeSubmission.user_id,
ResumeSubmission.question_id,
ResumeSubmission.answer,
)
.join(
resume_max_time,
(ResumeSubmission.user_id == resume_max_time.c.user_id)
& (ResumeSubmission.question_id == resume_max_time.c.question_id)
& (ResumeSubmission.created_at == resume_max_time.c.max_time),
)
.subquery("resume_latest")
)

resume_pivot = (
select(
resume_latest.c.user_id,
func.max(
case((resume_latest.c.question_id == q1_id, resume_latest.c.answer))
).label("q1_answer"),
func.max(
case((resume_latest.c.question_id == q2_id, resume_latest.c.answer))
).label("q2_answer"),
func.max(
case((resume_latest.c.question_id == q3_id, resume_latest.c.answer))
).label("q3_answer"),
)
.group_by(resume_latest.c.user_id)
.subquery("resume_pivot")
)

code_max_time = (
select(
CodeSubmission.user_id,
CodeSubmission.problem_id,
func.max(CodeSubmission.created_at).label("max_time"),
)
.where(CodeSubmission.problem_id.in_([p1_id, p2_id, p3_id]))
.group_by(CodeSubmission.user_id, CodeSubmission.problem_id)
.subquery("code_max_time")
)

latest_code = (
select(
CodeSubmission.id,
CodeSubmission.user_id,
CodeSubmission.problem_id,
CodeSubmission.source_code,
)
.join(
code_max_time,
(CodeSubmission.user_id == code_max_time.c.user_id)
& (CodeSubmission.problem_id == code_max_time.c.problem_id)
& (CodeSubmission.created_at == code_max_time.c.max_time),
)
.subquery("latest_code")
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

correct_counts = (
select(
CodeSubmissionResult.submission_id,
func.count(CodeSubmissionResult.testcase_id).label("correct_count"),
)
.where(CodeSubmissionResult.status == CodeSubmissionResultStatus.CORRECT)
.group_by(CodeSubmissionResult.submission_id)
.subquery("correct_counts")
)

code_result_join = (
select(
latest_code.c.user_id,
latest_code.c.problem_id,
latest_code.c.source_code,
func.coalesce(correct_counts.c.correct_count, 0).label("correct_count"),
)
.outerjoin(
correct_counts, correct_counts.c.submission_id == latest_code.c.id
)
.subquery("code_result_join")
)

code_pivot = (
select(
code_result_join.c.user_id,
func.coalesce(
func.max(
case(
(
code_result_join.c.problem_id == p1_id,
code_result_join.c.correct_count,
)
)
),
0,
).label("problem_1_correct"),
func.coalesce(
func.max(
case(
(
code_result_join.c.problem_id == p2_id,
code_result_join.c.correct_count,
)
)
),
0,
).label("problem_2_correct"),
func.coalesce(
func.max(
case(
(
code_result_join.c.problem_id == p3_id,
code_result_join.c.correct_count,
)
)
),
0,
).label("problem_3_correct"),
func.max(
case(
(
code_result_join.c.problem_id == p1_id,
code_result_join.c.source_code,
)
)
).label("problem_1_code"),
func.max(
case(
(
code_result_join.c.problem_id == p2_id,
code_result_join.c.source_code,
)
)
).label("problem_2_code"),
func.max(
case(
(
code_result_join.c.problem_id == p3_id,
code_result_join.c.source_code,
)
)
).label("problem_3_code"),
)
.group_by(code_result_join.c.user_id)
.subquery("code_pivot")
)

stmt = (
select(
User.first_name,
User.last_name,
User.university,
User.college,
User.department,
User.phone_number,
User.github_email,
User.slack_email,
User.notion_email,
resume_pivot.c.q1_answer,
resume_pivot.c.q2_answer,
resume_pivot.c.q3_answer,
code_pivot.c.problem_1_code,
code_pivot.c.problem_2_code,
code_pivot.c.problem_3_code,
func.coalesce(code_pivot.c.problem_1_correct, 0).label(
"problem_1_correct"
),
func.coalesce(code_pivot.c.problem_2_correct, 0).label(
"problem_2_correct"
),
func.coalesce(code_pivot.c.problem_3_correct, 0).label(
"problem_3_correct"
),
)
.join(RecruitingApplication, RecruitingApplication.user_id == User.id)
.outerjoin(resume_pivot, resume_pivot.c.user_id == User.id)
.outerjoin(code_pivot, code_pivot.c.user_id == User.id)
.where(RecruitingApplication.recruiting_id == recruiting_id)
.order_by(RecruitingApplication.created_at.desc())
.limit(limit)
.offset(offset)
)

result = self.session.execute(stmt).mappings().all()
return result
32 changes: 30 additions & 2 deletions wacruit/src/apps/recruiting/schemas.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from datetime import datetime
from typing import Literal, TYPE_CHECKING
from typing import TYPE_CHECKING
from typing import List
from typing import Literal
from typing import Optional

from pydantic import BaseModel

Expand All @@ -11,7 +14,7 @@
from wacruit.src.apps.resume.schemas import ResumeQuestionCreateDto

if TYPE_CHECKING:
from wacruit.src.apps.problem.models import Problem
pass


class RecruitingSummaryResponse(OrmModel):
Expand Down Expand Up @@ -135,3 +138,28 @@ class RecruitingInfoUpdateRequest(BaseModel):
info_num: int | None = None
title: str | None = None
date_info: str | None = None


class RecruitingSubmissionResponse(OrmModel):
first_name: Optional[str]
last_name: Optional[str]
university: Optional[str]
college: Optional[str]
department: Optional[str]
phone_number: Optional[str]
github_email: Optional[str]
slack_email: Optional[str]
notion_email: Optional[str]
q1_answer: Optional[str]
q2_answer: Optional[str]
q3_answer: Optional[str]
problem_1_code: Optional[str]
problem_2_code: Optional[str]
problem_3_code: Optional[str]
problem_1_correct: Optional[int]
problem_2_correct: Optional[int]
problem_3_correct: Optional[int]


class RecruitingSubmissionListResponse(OrmModel):
items: List[RecruitingSubmissionResponse]
13 changes: 13 additions & 0 deletions wacruit/src/apps/recruiting/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
from wacruit.src.apps.recruiting.schemas import RecruitingInfoUpdateRequest
from wacruit.src.apps.recruiting.schemas import RecruitingResponse
from wacruit.src.apps.recruiting.schemas import RecruitingResultResponse
from wacruit.src.apps.recruiting.schemas import RecruitingSubmissionListResponse
from wacruit.src.apps.recruiting.schemas import RecruitingSubmissionResponse
from wacruit.src.apps.recruiting.schemas import RecruitingSummaryResponse
from wacruit.src.apps.recruiting.schemas import RecruitingUpdateRequest
from wacruit.src.apps.recruiting.schemas import UserRecruitingResponse
Expand Down Expand Up @@ -277,3 +279,14 @@ def update_recruiting_info(
)
except IntegrityError as exc:
raise InfoAlreadyExistsException() from exc

def get_recruiting_submissions(
self, recruiting_id: int, limit: int, offset: int
) -> RecruitingSubmissionListResponse:
raw_submissions = self.recruiting_repository.get_submissions_by_recruiting_id(
recruiting_id, limit, offset
)

parsed_items = [RecruitingSubmissionResponse(**row) for row in raw_submissions]

return RecruitingSubmissionListResponse(items=parsed_items)
Comment on lines +283 to +292

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

recruiting_id 존재 여부 검증이 누락되었습니다.

다른 메서드들(get_recruiting_by_id, get_user_recruiting_by_id 등)과 달리 이 메서드는 recruiting_id의 유효성을 검증하지 않습니다. 존재하지 않는 recruiting_id로 호출해도 빈 리스트를 반환하여, 클라이언트가 "존재하지 않는 모집"과 "지원자가 없는 모집"을 구분할 수 없습니다.

🛡️ 제안하는 검증 추가
     def get_recruiting_submissions(
         self, recruiting_id: int, limit: int, offset: int
     ) -> RecruitingSubmissionListResponse:
+        recruiting = self.recruiting_repository.get_recruiting_by_id(recruiting_id)
+        if recruiting is None:
+            raise RecruitingNotFoundException()
+        
         raw_submissions = self.recruiting_repository.get_submissions_by_recruiting_id(
             recruiting_id, limit, offset
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wacruit/src/apps/recruiting/services.py` around lines 283 - 292,
get_recruiting_submissions currently returns an empty list for unknown
recruiting_id; add an existence check at the start by reusing the existing
validation logic (call self.get_recruiting_by_id(recruiting_id) or use the
repository existence/get method used by
get_recruiting_by_id/get_user_recruiting_by_id) and let it raise the same
NotFound/validation error when the recruiting doesn't exist, then proceed to
fetch submissions and build RecruitingSubmissionListResponse only if the
recruiting exists.

16 changes: 14 additions & 2 deletions wacruit/src/apps/recruiting/views_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@

from fastapi import APIRouter
from fastapi import Depends
from fastapi import Query

from wacruit.src.apps.common.enums import RecruitingType
from wacruit.src.apps.common.schemas import ListResponse
from wacruit.src.apps.recruiting.schemas import RecruitingCreateRequest
from wacruit.src.apps.recruiting.schemas import RecruitingInfoCreateRequest
from wacruit.src.apps.recruiting.schemas import RecruitingInfoResponse
from wacruit.src.apps.recruiting.schemas import RecruitingInfoUpdateRequest
from wacruit.src.apps.recruiting.schemas import RecruitingResponse
from wacruit.src.apps.recruiting.schemas import RecruitingSubmissionListResponse
from wacruit.src.apps.recruiting.schemas import RecruitingSummaryResponse
from wacruit.src.apps.recruiting.schemas import RecruitingUpdateRequest
from wacruit.src.apps.recruiting.services import RecruitingService
Expand All @@ -30,7 +31,7 @@ def create_recruiting(

@v3_router.get("/active")
def get_active_recruitings(
recruiting_service: Annotated[RecruitingService, Depends()]
recruiting_service: Annotated[RecruitingService, Depends()],
) -> ListResponse[RecruitingSummaryResponse]:
return recruiting_service.get_active_recruitings()

Expand Down Expand Up @@ -82,3 +83,14 @@ def update_recruiting_info(
request: RecruitingInfoUpdateRequest,
) -> RecruitingInfoResponse:
return recruiting_service.update_recruiting_info(recruiting_info_id, request)


@v3_router.get("/{recruiting_id}/submission")
def get_recruiting_submission(
admin_user: AdminUser,
recruiting_id: int,
recruiting_service: Annotated[RecruitingService, Depends()],
limit: int = Query(100),
offset: int = Query(0),
) -> RecruitingSubmissionListResponse:
return recruiting_service.get_recruiting_submissions(recruiting_id, limit, offset)
Loading