Skip to content

Commit 7f92884

Browse files
authored
Used global and score breakdowns for avg score in hacker summary (#691)
* Added function to include breakdown fields in summary * Changed function temporarily for route * Fixed unit tests * Removed redundant unit test
1 parent f6bb695 commit 7f92884

File tree

3 files changed

+149
-57
lines changed

3 files changed

+149
-57
lines changed

apps/api/src/admin/applicant_review_processor.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
}
1313

1414

15+
# TODO: Make this function only used for old applicant summary
16+
# meaning remove the resume_reviewed
1517
def include_hacker_app_fields(
1618
applicant_record: dict[str, Any], accept_threshold: float, waitlist_threshold: float
1719
) -> None:
@@ -23,6 +25,18 @@ def include_hacker_app_fields(
2325
_include_resume_reviewed(applicant_record)
2426

2527

28+
def include_hacker_app_fields_with_global_and_breakdown(
29+
applicant_record: dict[str, Any], accept_threshold: float, waitlist_threshold: float
30+
) -> None:
31+
"""For applicant summary where global and breakdown scores are used"""
32+
_include_decision_based_on_threshold_and_score_breakdown(
33+
applicant_record, accept_threshold, waitlist_threshold
34+
)
35+
_include_reviewers(applicant_record)
36+
_include_avg_score_with_global_and_breakdown(applicant_record)
37+
_include_resume_reviewed(applicant_record)
38+
39+
2640
def include_review_decision(applicant_record: dict[str, Any]) -> None:
2741
"""Sets the applicant's decision as the last submitted review decision or None."""
2842
reviews = applicant_record["application_data"]["reviews"]
@@ -43,6 +57,8 @@ def _get_last_score(reviewer: str, reviews: list[tuple[str, str, float]]) -> flo
4357
return NOT_FULLY_REVIEWED
4458

4559

60+
# TODO: Make this function only used for old applicant summary
61+
# meaning remove the global_field_scores usage
4662
def _get_avg_score(
4763
reviews: list[tuple[str, str, float]], global_field_scores: dict[str, Any]
4864
) -> float:
@@ -62,6 +78,31 @@ def _get_avg_score(
6278
return (last_score + last_score2) / 2
6379

6480

81+
def _get_avg_score_with_globals_and_breakdown(
82+
review_breakdowns: dict[str, dict[str, int]], global_field_scores: dict[str, int]
83+
) -> float:
84+
# Check global_field_scores first - if any value is less than 0,
85+
# return OVERQUALIFIED
86+
if global_field_scores and any(score < 0 for score in global_field_scores.values()):
87+
return OVERQUALIFIED
88+
89+
if len(review_breakdowns) < 2:
90+
return NOT_FULLY_REVIEWED
91+
92+
# Review breakdowns should be the most recent scores
93+
total_score = 2 * sum(global_field_scores.values())
94+
for breakdown in review_breakdowns.values():
95+
for field, score in breakdown.items():
96+
# TODO: Fields from global_field_scores should not be in breakdowns
97+
# This check should be removed once breakdown models remove global fields
98+
if field in global_field_scores:
99+
continue
100+
101+
total_score += score
102+
103+
return total_score / 2
104+
105+
65106
def _include_decision_based_on_threshold(
66107
applicant_record: dict[str, Any], accept: float, waitlist: float
67108
) -> None:
@@ -77,6 +118,21 @@ def _include_decision_based_on_threshold(
77118
applicant_record["decision"] = Decision.REJECTED
78119

79120

121+
def _include_decision_based_on_threshold_and_score_breakdown(
122+
applicant_record: dict[str, Any], accept: float, waitlist: float
123+
) -> None:
124+
avg_score = _get_avg_score_with_globals_and_breakdown(
125+
applicant_record["application_data"]["review_breakdown"],
126+
applicant_record["application_data"].get("global_field_scores", {}),
127+
)
128+
if avg_score >= accept:
129+
applicant_record["decision"] = Decision.ACCEPTED
130+
elif avg_score >= waitlist:
131+
applicant_record["decision"] = Decision.WAITLISTED
132+
else:
133+
applicant_record["decision"] = Decision.REJECTED
134+
135+
80136
def _include_reviewers(applicant_record: dict[str, Any]) -> None:
81137
unique_reviewers = get_unique_reviewers(applicant_record)
82138
applicant_record["reviewers"] = sorted(list(unique_reviewers))
@@ -89,6 +145,15 @@ def _include_avg_score(applicant_record: dict[str, Any]) -> None:
89145
)
90146

91147

148+
def _include_avg_score_with_global_and_breakdown(
149+
applicant_record: dict[str, Any]
150+
) -> None:
151+
applicant_record["avg_score"] = _get_avg_score_with_globals_and_breakdown(
152+
applicant_record["application_data"]["review_breakdown"],
153+
applicant_record["application_data"].get("global_field_scores", {}),
154+
)
155+
156+
92157
def _include_resume_reviewed(applicant_record: dict[str, Any]) -> None:
93158
global_field_scores = applicant_record["application_data"].get(
94159
"global_field_scores", {}

apps/api/src/routers/admin.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,9 @@ async def hacker_applicants(
174174
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
175175

176176
for record in records:
177-
applicant_review_processor.include_hacker_app_fields(
177+
# TODO: If we change back to old avg score for summary, change this function
178+
# applicant_review_processor.include_hacker_app_fields
179+
applicant_review_processor.include_hacker_app_fields_with_global_and_breakdown(
178180
record, thresholds["accept"], thresholds["waitlist"]
179181
)
180182

apps/api/tests/test_admin.py

Lines changed: 81 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -73,58 +73,6 @@ def test_restricted_admin_route_is_forbidden(
7373
assert res.status_code == 403
7474

7575

76-
@patch("services.mongodb_handler.retrieve", autospec=True)
77-
@patch("services.mongodb_handler.retrieve_one", autospec=True)
78-
def test_can_retrieve_applicants(
79-
mock_mongodb_handler_retrieve_one: AsyncMock,
80-
mock_mongodb_handler_retrieve: AsyncMock,
81-
) -> None:
82-
"""Test that the applicants summary can be processed."""
83-
84-
mock_mongodb_handler_retrieve_one.side_effect = [
85-
HACKER_REVIEWER_IDENTITY,
86-
{"accept": 8, "waitlist": 5},
87-
]
88-
mock_mongodb_handler_retrieve.return_value = [
89-
{
90-
"_id": "edu.uci.petr",
91-
"first_name": "Peter",
92-
"last_name": "Anteater",
93-
"status": "REVIEWED",
94-
"application_data": {
95-
"school": "UC Irvine",
96-
"submission_time": datetime(2023, 1, 12, 9, 0, 0),
97-
"reviews": [
98-
[datetime(2023, 1, 18), "edu.uci.alicia", 8],
99-
[datetime(2023, 1, 18), "edu.uci.albert", 9],
100-
],
101-
},
102-
},
103-
]
104-
105-
res = reviewer_client.get("/applicants/hackers")
106-
107-
assert res.status_code == 200
108-
mock_mongodb_handler_retrieve.assert_awaited_once()
109-
data = res.json()
110-
assert data == [
111-
{
112-
"_id": "edu.uci.petr",
113-
"first_name": "Peter",
114-
"last_name": "Anteater",
115-
"resume_reviewed": False,
116-
"avg_score": 8.5,
117-
"reviewers": ["edu.uci.albert", "edu.uci.alicia"],
118-
"status": "REVIEWED",
119-
"decision": "ACCEPTED",
120-
"application_data": {
121-
"school": "UC Irvine",
122-
"submission_time": "2023-01-12T09:00:00",
123-
},
124-
},
125-
]
126-
127-
12876
@patch("services.mongodb_handler.retrieve", autospec=True)
12977
@patch("services.mongodb_handler.retrieve_one", autospec=True)
13078
def test_cannot_retrieve_applicants_without_role(
@@ -334,6 +282,64 @@ def test_non_waitlisted_applicant_cannot_be_released(
334282
mock_mongodb_handler_update_one.assert_not_awaited()
335283

336284

285+
# TODO: Should uncomment once new applicant summary is created at different route
286+
# @patch("services.mongodb_handler.retrieve_one", autospec=True)
287+
# @patch("services.mongodb_handler.retrieve", autospec=True)
288+
# def test_hacker_applicants_returns_correct_applicants(
289+
# mock_mongodb_handler_retrieve: AsyncMock,
290+
# mock_mongodb_handler_retrieve_one: AsyncMock,
291+
# ) -> None:
292+
# """Test that the /applicants/hackers route returns correctly"""
293+
# returned_records: list[dict[str, object]] = [
294+
# {
295+
# "_id": "edu.uci.sydnee",
296+
# "first_name": "sydnee",
297+
# "last_name": "unknown",
298+
# "status": "REVIEWED",
299+
# "application_data": {
300+
# "school": "Hamburger University",
301+
# "submission_time": datetime(2023, 1, 12, 9, 0, 0),
302+
# "reviews": [
303+
# [datetime(2023, 1, 19), "edu.uci.alicia", 100],
304+
# [datetime(2023, 1, 19), "edu.uci.alicia2", 200],
305+
# ],
306+
# },
307+
# }
308+
# ]
309+
310+
# expected_records = [
311+
# {
312+
# "_id": "edu.uci.sydnee",
313+
# "first_name": "sydnee",
314+
# "last_name": "unknown",
315+
# "resume_reviewed": False,
316+
# "status": "REVIEWED",
317+
# "decision": "ACCEPTED",
318+
# "avg_score": 150.0,
319+
# "reviewers": ["edu.uci.alicia", "edu.uci.alicia2"],
320+
# "application_data": {
321+
# "school": "Hamburger University",
322+
# "submission_time": "2023-01-12T09:00:00",
323+
# },
324+
# },
325+
# ]
326+
327+
# returned_thresholds: dict[str, object] = {"accept": 12, "waitlist": 5}
328+
329+
# mock_mongodb_handler_retrieve.return_value = returned_records
330+
# mock_mongodb_handler_retrieve_one.side_effect = [
331+
# HACKER_REVIEWER_IDENTITY,
332+
# returned_thresholds,
333+
# ]
334+
335+
# res = reviewer_client.get("/applicants/hackers")
336+
337+
# assert res.status_code == 200
338+
# mock_mongodb_handler_retrieve.assert_awaited_once()
339+
# data = res.json()
340+
# assert data == expected_records
341+
342+
337343
@patch("services.mongodb_handler.retrieve_one", autospec=True)
338344
@patch("services.mongodb_handler.retrieve", autospec=True)
339345
def test_hacker_applicants_returns_correct_applicants(
@@ -351,9 +357,28 @@ def test_hacker_applicants_returns_correct_applicants(
351357
"school": "Hamburger University",
352358
"submission_time": datetime(2023, 1, 12, 9, 0, 0),
353359
"reviews": [
354-
[datetime(2023, 1, 19), "edu.uci.alicia", 100],
355-
[datetime(2023, 1, 19), "edu.uci.alicia2", 200],
360+
[datetime(2023, 1, 19), "edu.uci.alicia", 56],
361+
[datetime(2023, 1, 19), "edu.uci.alicia2", 60],
356362
],
363+
"review_breakdown": {
364+
"alicia": {
365+
"resume": 15,
366+
"elevator_pitch_saq": 6,
367+
"tech_experience_saq": 6,
368+
"learn_about_self_saq": 8,
369+
"pixel_art_saq": 16,
370+
"hackathon_experience": -1000,
371+
},
372+
"alicia2": {
373+
"resume": 15,
374+
"elevator_pitch_saq": 10,
375+
"tech_experience_saq": 10,
376+
"learn_about_self_saq": 10,
377+
"pixel_art_saq": 10,
378+
"hackathon_experience": 5,
379+
},
380+
},
381+
"global_field_scores": {"resume": 15, "hackathon_experience": 5},
357382
},
358383
}
359384
]
@@ -363,10 +388,10 @@ def test_hacker_applicants_returns_correct_applicants(
363388
"_id": "edu.uci.sydnee",
364389
"first_name": "sydnee",
365390
"last_name": "unknown",
366-
"resume_reviewed": False,
391+
"resume_reviewed": True,
367392
"status": "REVIEWED",
368393
"decision": "ACCEPTED",
369-
"avg_score": 150.0,
394+
"avg_score": 58.0,
370395
"reviewers": ["edu.uci.alicia", "edu.uci.alicia2"],
371396
"application_data": {
372397
"school": "Hamburger University",

0 commit comments

Comments
 (0)