1
1
import asyncio
2
2
from datetime import date , datetime
3
3
from logging import getLogger
4
- from typing import Annotated , Any , Literal , Optional , Sequence
4
+ from typing import Annotated , Any , Literal , Mapping , Optional , Sequence
5
5
6
6
from fastapi import APIRouter , Body , Depends , HTTPException , status
7
7
from pydantic import BaseModel , EmailStr , TypeAdapter , ValidationError
11
11
from admin .participant_manager import Participant
12
12
from auth .authorization import require_role
13
13
from auth .user_identity import User , utc_now
14
- from models .ApplicationData import Decision , OtherReview
14
+ from models .ApplicationData import Decision , Review
15
15
from models .user_record import Applicant , ApplicantStatus , Role , Status
16
16
from services import mongodb_handler , sendgrid_handler
17
17
from services .mongodb_handler import BaseRecord , Collection
@@ -48,11 +48,16 @@ class HackerApplicantSummary(BaseRecord):
48
48
last_name : str
49
49
status : str
50
50
decision : Optional [Decision ] = None
51
- num_reviewers : int
51
+ reviewers : list [ str ] = []
52
52
avg_score : float
53
53
application_data : ApplicationDataSummary
54
54
55
55
56
+ class ReviewRequest (BaseModel ):
57
+ applicant : str
58
+ score : float
59
+
60
+
56
61
@router .get ("/applicants" )
57
62
async def applicants (
58
63
user : Annotated [User , Depends (require_manager )]
@@ -104,9 +109,7 @@ async def hacker_applicants(
104
109
],
105
110
)
106
111
107
- thresholds : Optional [dict [str , float ]] = await mongodb_handler .retrieve_one (
108
- Collection .SETTINGS , {"_id" : "hacker_score_thresholds" }, ["accept" , "waitlist" ]
109
- )
112
+ thresholds : Optional [dict [str , float ]] = await _retrieve_thresholds ()
110
113
111
114
if not thresholds :
112
115
log .error ("Could not retrieve thresholds" )
@@ -164,27 +167,62 @@ async def applications(
164
167
165
168
@router .post ("/review" )
166
169
async def submit_review (
167
- applicant : str = Body (),
168
- decision : Decision = Body (),
170
+ applicant_review : ReviewRequest ,
169
171
reviewer : User = Depends (require_role ({Role .REVIEWER })),
170
172
) -> None :
171
- """Submit a review decision from the reviewer for the given applicant."""
172
- log .info ("%s reviewed applicant %s" , reviewer , applicant )
173
+ """Submit a review decision from the reviewer for the given hacker applicant."""
174
+ log .info ("%s reviewed hacker %s" , reviewer , applicant_review . applicant )
173
175
174
- review : OtherReview = (utc_now (), reviewer .uid , decision )
176
+ review : Review = (utc_now (), reviewer .uid , applicant_review .score )
177
+ app = applicant_review .applicant
175
178
176
- try :
177
- await mongodb_handler .raw_update_one (
178
- Collection .USERS ,
179
- {"_id" : applicant },
180
- {
179
+ applicant_record = await mongodb_handler .retrieve_one (
180
+ Collection .USERS ,
181
+ {"_id" : applicant_review .applicant },
182
+ ["_id" , "application_data.reviews" , "roles" ],
183
+ )
184
+ if not applicant_record :
185
+ log .error ("Could not retrieve applicant after submitting review" )
186
+ raise HTTPException (status .HTTP_500_INTERNAL_SERVER_ERROR )
187
+
188
+ if Role .HACKER in applicant_record ["roles" ]:
189
+ unique_reviewers = applicant_review_processor .get_unique_reviewers (
190
+ applicant_record
191
+ )
192
+
193
+ # Only add a review if there are either less than 2 reviewers
194
+ # or reviewer is one of the reviewers
195
+ if len (unique_reviewers ) >= 2 and reviewer .uid not in unique_reviewers :
196
+ log .error (
197
+ "%s tried to submit a review, but %s already has two reviewers" ,
198
+ reviewer ,
199
+ app ,
200
+ )
201
+ raise HTTPException (status .HTTP_403_FORBIDDEN )
202
+
203
+ update_query : dict [str , object ] = {
204
+ "$push" : {"application_data.reviews" : review }
205
+ }
206
+ # Because reviewing a hacker requires 2 reviewers, only set the
207
+ # applicant's status to REVIEWED if there are at least 2 reviewers
208
+ if len (unique_reviewers | {reviewer .uid }) >= 2 :
209
+ update_query .update ({"$set" : {"status" : "REVIEWED" }})
210
+
211
+ await _try_update_applicant_with_query (
212
+ applicant_review ,
213
+ update_query = update_query ,
214
+ err_msg = f"{ reviewer } could not submit review for { app } " ,
215
+ )
216
+
217
+ else :
218
+ await _try_update_applicant_with_query (
219
+ applicant_review ,
220
+ update_query = {
181
221
"$push" : {"application_data.reviews" : review },
182
222
"$set" : {"status" : "REVIEWED" },
183
223
},
224
+ err_msg = f"{ reviewer } could not submit review for { app } " ,
184
225
)
185
- except RuntimeError :
186
- log .error ("Could not submit review for %s" , applicant )
187
- raise HTTPException (status .HTTP_500_INTERNAL_SERVER_ERROR )
188
226
189
227
190
228
@router .post ("/release" , dependencies = [Depends (require_director )])
@@ -199,14 +237,32 @@ async def release_decisions() -> None:
199
237
for record in records :
200
238
applicant_review_processor .include_review_decision (record )
201
239
202
- for decision in (Decision .ACCEPTED , Decision .WAITLISTED , Decision .REJECTED ):
203
- group = [record for record in records if record ["decision" ] == decision ]
204
- if not group :
205
- continue
206
- await asyncio .gather (
207
- * (_process_batch (batch , decision ) for batch in batched (group , 100 ))
240
+ await _process_records_in_batches (records )
241
+
242
+
243
+ # TODO: need to make release hackers check roles as part of query
244
+ @router .post ("/release/hackers" , dependencies = [Depends (require_director )])
245
+ async def release_hacker_decisions () -> None :
246
+ """Update hacker applicant status based on decision and send decision emails."""
247
+ records = await mongodb_handler .retrieve (
248
+ Collection .USERS ,
249
+ {"status" : Status .REVIEWED },
250
+ ["_id" , "application_data.reviews" , "first_name" ],
251
+ )
252
+
253
+ thresholds : Optional [dict [str , float ]] = await _retrieve_thresholds ()
254
+
255
+ if not thresholds :
256
+ log .error ("Could not retrieve thresholds" )
257
+ raise HTTPException (status .HTTP_500_INTERNAL_SERVER_ERROR )
258
+
259
+ for record in records :
260
+ applicant_review_processor .include_hacker_app_fields (
261
+ record , thresholds ["accept" ], thresholds ["waitlist" ]
208
262
)
209
263
264
+ await _process_records_in_batches (records )
265
+
210
266
211
267
@router .post ("/rsvp-reminder" , dependencies = [Depends (require_director )])
212
268
async def rsvp_reminder () -> None :
@@ -360,6 +416,16 @@ async def subevent_checkin(
360
416
await participant_manager .subevent_checkin (event , uid , organizer )
361
417
362
418
419
+ async def _process_records_in_batches (records : list [dict [str , object ]]) -> None :
420
+ for decision in (Decision .ACCEPTED , Decision .WAITLISTED , Decision .REJECTED ):
421
+ group = [record for record in records if record ["decision" ] == decision ]
422
+ if not group :
423
+ continue
424
+ await asyncio .gather (
425
+ * (_process_batch (batch , decision ) for batch in batched (group , 100 ))
426
+ )
427
+
428
+
363
429
async def _process_status (uids : Sequence [str ], status : Status ) -> None :
364
430
ok = await mongodb_handler .update (
365
431
Collection .USERS , {"_id" : {"$in" : uids }}, {"status" : status }
@@ -397,3 +463,26 @@ def _recover_email_from_uid(uid: str) -> str:
397
463
local = local .replace ("\n " , "." )
398
464
domain = "." .join (reversed (reversed_domain ))
399
465
return f"{ local } @{ domain } "
466
+
467
+
468
+ async def _retrieve_thresholds () -> Optional [dict [str , Any ]]:
469
+ return await mongodb_handler .retrieve_one (
470
+ Collection .SETTINGS , {"_id" : "hacker_score_thresholds" }, ["accept" , "waitlist" ]
471
+ )
472
+
473
+
474
+ async def _try_update_applicant_with_query (
475
+ applicant_review : ReviewRequest ,
476
+ * ,
477
+ update_query : Mapping [str , object ],
478
+ err_msg : str = "" ,
479
+ ) -> None :
480
+ try :
481
+ await mongodb_handler .raw_update_one (
482
+ Collection .USERS ,
483
+ {"_id" : applicant_review .applicant },
484
+ update_query ,
485
+ )
486
+ except RuntimeError :
487
+ log .error (err_msg )
488
+ raise HTTPException (status .HTTP_500_INTERNAL_SERVER_ERROR )
0 commit comments