Coverage for middle_layer/review/application_layer/rest_api/views/individual_science_review.py: 93.16%

263 statements  

« prev     ^ index     » next       coverage.py v7.10.5, created at 2026-03-09 06:13 +0000

1# Copyright 2024 Associated Universities, Inc. 

2# 

3# This file is part of Telescope Time Allocation Tools (TTAT). 

4# 

5# TTAT is free software: you can redistribute it and/or modify 

6# it under the terms of the GNU General Public License as published by 

7# the Free Software Foundation, either version 3 of the License, or 

8# any later version. 

9# 

10# TTAT is distributed in the hope that it will be useful, 

11# but WITHOUT ANY WARRANTY; without even the implied warranty of 

12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

13# GNU General Public License for more details. 

14# 

15# You should have received a copy of the GNU General Public License 

16# along with TTAT. If not, see <https://www.gnu.org/licenses/>. 

17# 

18from http import HTTPStatus 

19 

20from pyramid.httpexceptions import ( 

21 HTTPBadRequest, 

22 HTTPInternalServerError, 

23 HTTPNotFound, 

24 HTTPPreconditionFailed, 

25 HTTPUnauthorized, 

26) 

27from pyramid.request import Request 

28from pyramid.response import Response 

29from pyramid.view import view_config 

30 

31from auth.auth import get_user_by_id 

32from common.application_layer.rest_api import make_expected_params_message 

33from propose.domain_layer.entities.proposal import Proposal 

34from review.application_layer.services.finalize_isrs import individual_science_reviews_finalize 

35from review.application_layer.services.notify_tta_members_srp_isrs_finalized import ( 

36 notify_tta_members_srp_isrs_finalized, 

37) 

38from review.domain_layer.entities.conflict_declaration import ConflictDeclaration 

39from review.domain_layer.entities.individual_science_review import IndividualScienceReview 

40from review.domain_layer.entities.science_review_panel import ScienceReviewPanel 

41from review.domain_layer.entities.science_reviewer import ScienceReviewer 

42from review.domain_layer.services.anonymize_isr_service import anonymize_isrs 

43from review.domain_layer.services.deserialize_csv_to_isrs_service import deserialize_csv_to_isrs 

44from review.domain_layer.services.is_srp_in_consensus_phase_service import is_in_consensus_phase 

45from review.domain_layer.services.serialize_isrs_to_csv_service import serialize_isrs_to_csv 

46from review.domain_layer.services.validate_isr_score_service import ( 

47 INDIVIDUAL_REV_SCORE_RANGE_MESSAGE, 

48 validate_isr_score, 

49) 

50from review.domain_layer.services.validate_isr_state_change_service import validate_isr_state_change 

51 

52 

53@view_config( 

54 route_name="individual_science_review_assignment", 

55 renderer="json", 

56 permission="individual_science_review_assignment", 

57) 

58def individual_science_review_assignment(request: Request) -> Response: 

59 """Update the review types, but not the comments or scores, of a list of IndividualScienceReview objects, 

60 

61 URL: individual_science_reviews 

62 

63 :param request: PUT request with JSON object like: 

64 [{ 

65 "individualScienceReviewId": <int>, 

66 "reviewType": <str>, 

67 }] 

68 :return: Response with a list of JSON-formatted IndividualScienceReview objects 

69 or 400 response (HTTPBadRequest) if expected parameters not given 

70 or an invalid isr is specified, 

71 or multiple primary or secondary reviewers were specified for a given ISR 

72 or there are isrs from multiple SRP sent 

73 or 401 response HTTPUnauthorized if the user requesting the update does not have permission 

74 or 404 response (HTTPNotFound) if isr specified or requester/reviewer associated with the isr cannot be found 

75 or 412 response (HTTPPreconditionFailed) if the ISR is conflicted (Automatically Conflicted or Conflicted) 

76 Note: if any isr change request results in an error, that error is immediately returned to the 

77 caller. Given this, it is possible that a list of updates will error on the first problem found, that issue 

78 can be corrected, and a later update request in the list will also error 

79 """ 

80 params = request.json_body 

81 updated_isrs: list[IndividualScienceReview] = list() 

82 

83 tta_member = False 

84 

85 # the user requesting an assignment be performed. Needed to determine their role relative to the 

86 # associated SRP and ISR 

87 requestor: ScienceReviewer = None 

88 

89 if request.identity and "tta_member" in request.identity.roles: 

90 tta_member = True 

91 

92 isrs_to_update = list() 

93 

94 if len(params) == 0: 

95 return Response(status_code=HTTPStatus.OK, json=[]) 

96 

97 isr: IndividualScienceReview = request.lookup(params[0]["individualScienceReviewId"], IndividualScienceReview) 

98 first_panel: ScienceReviewPanel = request.repo.science_review_panel_repo.by_proposal_id(isr.proposal_id) 

99 isrs_on_panel = request.repo.individual_science_review_repo.list_by_srp_id(first_panel.science_review_panel_id) 

100 

101 # Verify that request user has permissions to perform assignment 

102 if not tta_member: 

103 if not requestor: # get the requestor data: 

104 # Verify that the requester is a chair on the panel that is reviewing this proposal 

105 requestor = request.repo.science_reviewer_repo.by_srp_id_and_user_id( 

106 first_panel.science_review_panel_id, request.identity.user_id 

107 ) 

108 if not requestor or not requestor.is_chair: 

109 raise HTTPUnauthorized(body=f"user {request.identity.user_id} is not authorized to set ISR type.") 

110 

111 # build a matrix of the existing isrs: 

112 isrs_by_proposal = dict() 

113 for isr in isrs_on_panel: 

114 isrs_by_proposal.setdefault(isr.proposal_id, {})[isr.individual_science_review_id] = isr 

115 

116 for isr_json in params: 

117 expected_params = ["individualScienceReviewId", "reviewType"] 

118 if not all([expected in isr_json for expected in expected_params]): 

119 # JSON params do not contain all expected params 

120 raise HTTPBadRequest(body=make_expected_params_message(expected_params, isr_json.keys())) 

121 

122 isr: IndividualScienceReview = request.lookup(isr_json["individualScienceReviewId"], IndividualScienceReview) 

123 panel: ScienceReviewPanel = request.repo.science_review_panel_repo.by_proposal_id(isr.proposal_id) 

124 if panel != first_panel: 

125 raise HTTPBadRequest( 

126 body=f"ISR {isr.individual_science_review_id} is not a part of Science Review Panel " 

127 f"{first_panel.science_review_panel_name}. " 

128 f"All ISRs assigned together should be part of the same SRP." 

129 ) 

130 

131 # Verify that ISR is not conflicted 

132 if isr.conflict_declaration.conflict_state != "Available": 

133 raise HTTPPreconditionFailed( 

134 body=f"Expected individual science review conflict declaration to be available. Found a conflict " 

135 f"state of {isr.conflict_declaration.conflict_state} for ISR id {isr.individual_science_review_id}" 

136 ) 

137 

138 reviewer_associated_with_isr: ScienceReviewer = request.repo.science_reviewer_repo.by_id( 

139 isr.science_reviewer_id 

140 ) 

141 

142 # Before proceeding, ensure that the reviewer associated with this ISR, and not being an external reviewer 

143 # has certified conflicts 

144 if ( 

145 reviewer_associated_with_isr.science_review_panel_id == panel.science_review_panel_id 

146 and not reviewer_associated_with_isr.are_conflicts_certified 

147 ): 

148 raise HTTPPreconditionFailed( 

149 body=f"Expected the reviewer associated with individual science review to " 

150 f"have certified all conflicts. ISR id {isr.individual_science_review_id}, Science Reviewer id" 

151 f" {isr.science_reviewer_id}" 

152 ) 

153 

154 isr.review_type = isr_json["reviewType"] 

155 isrs_to_update.append(isr) 

156 isrs_by_proposal[isr.proposal_id][isr.individual_science_review_id] = isr 

157 

158 # check that the total of changes does not give any proposal multiple primaries or secondaries: 

159 for proposal_id in isrs_by_proposal: 

160 primaries = 0 

161 secondaries = 0 

162 

163 # check each proposal for only one primary/secondary 

164 for isr_id in isrs_by_proposal[proposal_id]: 

165 if isrs_by_proposal[proposal_id][isr_id].review_type == "Primary": 

166 if primaries > 0: 

167 raise HTTPBadRequest(body=f"Multiple primary reviewers for proposal {isr.proposal_id}") 

168 else: 

169 primaries = primaries + 1 

170 if isrs_by_proposal[proposal_id][isr_id].review_type == "Secondary": 

171 if secondaries > 0: 

172 raise HTTPBadRequest(body=f"Multiple secondary reviewers for proposal {isr.proposal_id}") 

173 else: 

174 secondaries = secondaries + 1 

175 

176 for isr in isrs_to_update: 

177 request.repo.individual_science_review_repo.update(isr) 

178 updated_isrs.append(isr) 

179 

180 return Response(status_code=HTTPStatus.OK, json=[isr.__json__() for isr in updated_isrs]) 

181 

182 

183@view_config( 

184 route_name="individual_science_review_external_assignment", 

185 renderer="json", 

186 permission="individual_science_review_external_assignment", 

187) 

188def individual_science_review_external_assignment(request: Request) -> Response: 

189 """Create an external IndividualScienceReview and ConflictDeclaration for a given ScienceReviewer and Proposal 

190 

191 Where an external ISR exists on its Proposal's SRP, and its Proposal and ScienceReviewer belong to different SRPs 

192 

193 :param request: POST request with JSON body like: 

194 { 

195 "proposalId": <int>, 

196 "scienceReviewerId": <int>, 

197 "reviewType": <str> 

198 } 

199 :return: Response with JSON-formatted blank ISR for the given Proposal and ScienceReviewer, on the former's SRP 

200 or 404 response (HTTPNotFound) if the Proposal or ScienceReviewer doesn't exist with the specified IDs 

201 or 400 (HTTPBadRequest) response if proposalId or scienceReviewerId is not an integer, 

202 or the Proposal would have too many Primary or Secondary ISRs, 

203 or the Proposal isn't assigned to an SRP, 

204 or the Proposal and ScienceReviewer belong to the same SRP 

205 or the Proposal's SRP is in Consensus phase 

206 or 401 (HTTPUnauthorized) response if the requesting user isn't a TTA member 

207 """ 

208 params = request.json_body 

209 expected_params = ["proposalId", "scienceReviewerId", "reviewType"] 

210 if not all([expected in params for expected in expected_params]): 

211 raise HTTPBadRequest(body=make_expected_params_message(expected_params, params.keys())) 

212 reviewer: ScienceReviewer = request.lookup(params["scienceReviewerId"], ScienceReviewer) 

213 reviewer_panel: ScienceReviewPanel = request.lookup(reviewer.science_review_panel_id, ScienceReviewPanel) 

214 proposal_panel: ScienceReviewPanel = request.repo.science_review_panel_repo.by_proposal_id(params["proposalId"]) 

215 proposal: Proposal = request.lookup(params["proposalId"], Proposal) 

216 if proposal_panel is None: 

217 raise HTTPBadRequest(body=f"Expected Proposal with ID {proposal.proposal_id} to belong to an SRP, found None") 

218 if request.repo.ppr_proposal_review_repo.list_by_srp_id(proposal_panel.science_review_panel_id): 

219 raise HTTPBadRequest( 

220 body=f"Expected the Proposal {proposal.proposal_code}'s SRP ({proposal_panel.science_review_panel_id}) " 

221 f"to not be in Consensus phase" 

222 ) 

223 if proposal_panel == reviewer_panel: 

224 raise HTTPBadRequest( 

225 body=f"Expected ScienceReviewer and Proposal to belong to different SRPs, found that both belong to " 

226 f"{reviewer_panel.science_review_panel_id}" 

227 ) 

228 if proposal_panel.solicitation.solicitation_id != reviewer_panel.solicitation.solicitation_id: 

229 raise HTTPBadRequest( 

230 f"Expected ScienceReviewer and Proposal to belong to SRPs on the same Solicitation, " 

231 f"found that the former is on '{reviewer_panel.solicitation.solicitation_name}' while the latter is on " 

232 f"'{proposal_panel.solicitation.solicitation_name}'" 

233 ) 

234 if params["reviewType"] == "Primary" or params["reviewType"] == "Secondary": 

235 existing_prims_or_secs = request.repo.science_reviewer_repo.list_by_proposal_id_and_review_type( 

236 proposal.proposal_id, params["reviewType"] 

237 ) 

238 if existing_prims_or_secs: 

239 raise HTTPBadRequest( 

240 f"Expected Proposal with ID {proposal.proposal_id} to not have an existing ISR with " 

241 f"ReviewType {params['reviewType']}, found one for ScienceReviewer " 

242 f"with User ID {existing_prims_or_secs[0].user_id}" 

243 ) 

244 

245 isr = IndividualScienceReview( 

246 reviewer, 

247 review_type=params["reviewType"], 

248 ) 

249 proposal.individual_science_reviews.append(isr) 

250 request.repo.individual_science_review_repo.add(isr) 

251 conflict_declaration = ConflictDeclaration(conflict_state="Available") 

252 isr.conflict_declaration = conflict_declaration 

253 request.repo.conflict_declaration_repo.add(conflict_declaration) 

254 return Response(status_code=HTTPStatus.OK, json=isr.__json__()) 

255 

256 

257@view_config( 

258 route_name="individual_science_review_update", 

259 renderer="json", 

260 permission="individual_science_review_update", 

261) 

262def individual_science_review_update(request: Request) -> Response: 

263 """Enter an IndividualScienceReview by updating its comments, state, or scores 

264 

265 URL: individual_science_reviews/{isr_id} 

266 

267 :param request: PUT Request with JSON body like: 

268 { 

269 "individualScore": <float>, 

270 "reviewState": <ReviewState>, 

271 "commentsForTheSrp": <str>, 

272 } 

273 where ReviewState is one of "Saved" or "Completed" 

274 :return: Response with JSON-formatted IndividualScienceReview updated according to the request 

275 or 404 response (HTTPNotFound) if no ISR exists with an ID of isr_id 

276 or 400 response (HTTPBadRequest) if expected parameters not given, 

277 or if isr_id is not an integer, 

278 or an invalid state change is requested 

279 or an invalid score is provided 

280 or 401 response (HTTPUnauthorized) if the requesting user doesn't have permission 

281 or 500 response (HTTPInternalServerError) if that there are insufficient SRs for one or more Proposls, 

282 and the notification saying so fails to send 

283 """ 

284 is_tta_member = bool(request.identity) and "tta_member" in request.identity.roles 

285 

286 isr: IndividualScienceReview = request.lookup(request.matchdict["isr_id"], IndividualScienceReview) 

287 

288 if not is_tta_member: 

289 reviewer = request.repo.science_reviewer_repo.by_id(isr.science_reviewer_id) 

290 user_id = request.identity.user_id 

291 if reviewer.user_id != user_id: 

292 raise HTTPUnauthorized( 

293 body=f"User {user_id} is not authorized to enter ISR {isr.individual_science_review_id}" 

294 ) 

295 if isr.review_state == "Closed": 

296 raise HTTPUnauthorized(body=f"Only TTA Members are authorized to update Closed ISRs") 

297 if isr.conflict_declaration.conflict_state in ["Conflicted", "AutomaticallyConflicted"]: 

298 raise HTTPUnauthorized( 

299 f"Only TTA Members are authorized to update ISRs with {isr.conflict_declaration.conflict_state} " 

300 f"ConflictDeclarations" 

301 ) 

302 

303 params = request.json_body 

304 expected_params = ["individualScore", "reviewState", "commentsForTheSrp", "conflictDeclaration"] 

305 if not all([expected in params for expected in expected_params]): 

306 raise HTTPBadRequest(body=make_expected_params_message(expected_params, params.keys())) 

307 

308 if not is_tta_member and request.identity.user_id != reviewer.user_id: 

309 raise HTTPUnauthorized(body=f"Insufficient permission to complete operation.") 

310 

311 if ( 

312 isr.comments_for_the_srp != params["commentsForTheSrp"] 

313 or isr.individual_score != params["individualScore"] 

314 or isr.review_state != params["reviewState"] 

315 ): 

316 # validate and update the isr: 

317 validation_message = validate_isr_state_change( 

318 isr.review_state, params["reviewState"], is_tta_member, is_finalize_call=False 

319 ) 

320 if validation_message: 

321 raise HTTPBadRequest(body=validation_message) 

322 if not validate_isr_score(params["individualScore"]): 

323 raise HTTPBadRequest(body=f"ISR {INDIVIDUAL_REV_SCORE_RANGE_MESSAGE}") 

324 isr.comments_for_the_srp = params["commentsForTheSrp"] 

325 isr.individual_score = params["individualScore"] 

326 isr.review_state = params["reviewState"] 

327 if isr.review_state == "Closed": 

328 isr.review_type = "None" # To allow assigning other ISRs as Primary or Secondary if this one was 

329 

330 request.repo.individual_science_review_repo.update(isr) 

331 

332 return Response(json=isr.__json__()) 

333 

334 

335@view_config( 

336 route_name="individual_science_review_finalize", 

337 renderer="json", 

338 permission="individual_science_review_finalize", 

339) 

340def individual_science_review_finalize(request: Request) -> Response: 

341 """Finalize a ScienceReviewer's IndividualScienceReviews 

342 

343 URL: individual_science_reviews/finalize/{reviewer_id} 

344 

345 Note: If all ISRs for the SRP are discovered to be finalized, a notification is sent to TTA members 

346 

347 :param request: PUT Request 

348 :return: Response with JSON of the updated ISRs 

349 or 404 response (HTTPNotFound) if no reviewer exists with an ID of reviewerIid 

350 or 400 response (HTTPBadRequest) if reviewerId is not an integer or not present 

351 or ISRs for the reviewer could not be finalized 

352 or 401 response (HTTPUnauthorized) if the requesting user does not have permission to finalize 

353 the given reviewer 

354 or 500 response (HTTPInternalServerError) if all ISRs for the SRP are discovered to be finalized, 

355 and the notification saying so fails to send 

356 """ 

357 is_tta_member = bool(request.identity) and "tta_member" in request.identity.roles 

358 

359 reviewer: ScienceReviewer = request.lookup(request.matchdict["reviewer_id"], ScienceReviewer) 

360 

361 if not is_tta_member: # verify that the reviewer is attached to the user requesting the action: 

362 if reviewer.user_id != request.identity.user_id: 

363 raise HTTPUnauthorized( 

364 body=f"User {request.identity.user_id} is not authorized to finalize individual science reviews " 

365 f"for {reviewer.user_id}" 

366 ) 

367 

368 try: 

369 updated_isrs = individual_science_reviews_finalize(reviewer, is_tta_member, request.repo) 

370 except ValueError as e: 

371 raise HTTPBadRequest(body=str(e)) 

372 

373 # Determine the SRPs associated with the reviewers whose ISRs were finalized 

374 srs: list[ScienceReviewer] = [ 

375 request.repo.science_reviewer_repo.by_id(isr.science_reviewer_id) for isr in updated_isrs 

376 ] 

377 srp_ids = set(sr.science_review_panel_id for sr in srs) 

378 

379 # Notify TTA members if all ISRs have been finalized for the SRPs associated with the updated ISRs 

380 try: 

381 for srp_id in srp_ids: 

382 srp: ScienceReviewPanel = request.lookup(srp_id, ScienceReviewPanel) 

383 notify_tta_members_srp_isrs_finalized(srp, request.repo) 

384 except RuntimeError as e: 

385 raise HTTPInternalServerError(body=str(e)) 

386 

387 new_isrs = request.repo.individual_science_review_repo.list_by_reviewer_id(reviewer.science_reviewer_id) 

388 response_json = [isr.__json__() for isr in new_isrs] 

389 

390 return Response(json=response_json) 

391 

392 

393@view_config( 

394 route_name="individual_science_review_update_fnis", 

395 renderer="json", 

396 permission="individual_science_review_update", 

397) 

398def individual_science_review_update_fnis(request: Request) -> Response: 

399 """Update the finalized normalized scores of a list of IndividualScienceReviews. 

400 

401 Intended to be called by the chair during Consensus phase. 

402 

403 URL: individual_science_reviews/update_fnis 

404 

405 :param request: PUT Request with list of JSON objects with individualScienceReviewId and finalizedNormalizedScore 

406 :return: Response with the json of the updated ISRs 

407 or 404 response (HTTPNotFound) if no isr exists with an ID of individualScienceReviewerId 

408 or 400 response (HTTPBadRequest) if an individualScienceReviewId is invalid, 

409 there is no list of ISRs or they do not include the required fields, 

410 or ISRs associated with multiple science panels are found 

411 or any of the scores are not between 0.1 and 9.9 in increments of a tenth 

412 or 401 response (HTTPUnauthorized) if the requesting user is not a chair or tta member, 

413 or the ISRs are not on the chair's panel 

414 """ 

415 is_tta_member = bool(request.identity) and "tta_member" in request.identity.roles 

416 

417 params = request.json_body 

418 expected_params = ["individualScienceReviewId", "finalizedNormalizedScore"] 

419 for isr_json in params: 

420 if not all([expected in isr_json for expected in expected_params]): 

421 raise HTTPBadRequest(body=make_expected_params_message(expected_params, isr_json.keys())) 

422 

423 srp_ids = set() 

424 for isr_json in params: 

425 isr: IndividualScienceReview = request.lookup(isr_json["individualScienceReviewId"], IndividualScienceReview) 

426 srp: ScienceReviewPanel = request.repo.science_review_panel_repo.by_proposal_id(isr.proposal_id) 

427 srp_ids.add(srp.science_review_panel_id) 

428 if len(srp_ids) > 1: # If multiple SRPs are found to be present, raise an error. 

429 raise HTTPBadRequest(f"ISRs found for multiple Science Review Panels") 

430 

431 srp_id = next(iter(srp_ids)) 

432 panel = request.repo.science_review_panel_repo.by_id(srp_id) 

433 

434 user_id = request.identity.user_id 

435 reviewer: ScienceReviewer | None = request.repo.science_reviewer_repo.by_srp_id_and_user_id( 

436 panel.science_review_panel_id, user_id 

437 ) 

438 if not is_tta_member: 

439 if reviewer is None or reviewer.is_chair is False: 

440 raise HTTPUnauthorized( 

441 body=f"User {user_id} is not authorized to updated finalized normalized scores " 

442 f"for panel {panel.science_review_panel_id}" 

443 ) 

444 

445 # set the FNIS for each ISR sent: 

446 updated_isrs: list[IndividualScienceReview] = list() 

447 try: 

448 for isr_json in params: 

449 isr = request.repo.individual_science_review_repo.by_id(isr_json["individualScienceReviewId"]) 

450 if reviewer is not None: 

451 reviewers_cd: ConflictDeclaration | None = ( 

452 request.repo.conflict_declaration_repo.by_reviewer_id_and_proposal_id( 

453 reviewer.science_reviewer_id, isr.proposal_id 

454 ) 

455 ) 

456 if reviewers_cd is not None and reviewers_cd.conflict_state in [ 

457 "Conflicted", 

458 "AutomaticallyConflicted", 

459 ]: 

460 raise HTTPUnauthorized( 

461 body=f"ScienceReviewer {reviewer.science_reviewer_id} is not permitted to update an ISR " 

462 f"for Proposal {isr.proposal_id} since they have " 

463 f"a ConflictState of {reviewers_cd.conflict_state} on it" 

464 ) 

465 

466 # Updating the FNIS during the consensus stage, the entry must also be between 0.1 and 9.9 in tenths 

467 if not validate_isr_score(isr_json["finalizedNormalizedScore"]): 

468 raise HTTPBadRequest( 

469 body=f"Updated finalized {INDIVIDUAL_REV_SCORE_RANGE_MESSAGE}, received {isr_json["finalizedNormalizedScore"]}" 

470 ) 

471 

472 isr.finalized_normalized_score = isr_json["finalizedNormalizedScore"] 

473 request.repo.individual_science_review_repo.update(isr) 

474 updated_isrs.append(isr) 

475 except ValueError as e: 

476 raise HTTPNotFound(body=str(e)) 

477 

478 anonymized_isrs = anonymize_isrs( 

479 request.repo, 

480 updated_isrs, 

481 reviewer, 

482 is_tta_member, 

483 is_consensus_phase=is_in_consensus_phase(panel, request.repo), 

484 ) 

485 return Response(json=anonymized_isrs) 

486 

487 

488@view_config(route_name="individual_science_review_export", permission="individual_science_review_export") 

489def individual_science_review_export(request: Request) -> Response: 

490 """Serve a ScienceReviewer's IndividualScienceReviews as a CSV file 

491 

492 URL: science_reviewer/{reviewer_id}/export_isrs 

493 

494 :param request: GET request with the ScienceReviewer's ID 

495 :return: A response with the given ScienceReviewer's ISRs as a CSV file, 

496 or 401 response HTTPUnauthorized if the requesting User isn't either the given ScienceReviewer's User 

497 or a TTA member 

498 or 404 response (HTTPNotFound) if no Reviewer exists with an ID of reviewer_id 

499 or 400 response (HTTPBadRequest) if reviewer_id is not an integer 

500 """ 

501 is_tta_member = bool(request.identity) and "tta_member" in request.identity.roles 

502 reviewer: ScienceReviewer = request.lookup(request.matchdict["reviewer_id"], ScienceReviewer) 

503 

504 if not is_tta_member: # verify that the reviewer is attached to the user requesting the action: 

505 if reviewer.user_id != request.identity.user_id: 

506 raise HTTPUnauthorized( 

507 body=f"User {request.identity.user_id} is not authorized to export science reviews " 

508 f"for {reviewer.user_id}" 

509 ) 

510 

511 isrs: list[IndividualScienceReview] = request.repo.individual_science_review_repo.list_by_reviewer_id( 

512 reviewer.science_reviewer_id 

513 ) 

514 isr_csv = serialize_isrs_to_csv(isrs, request.repo) 

515 filename = f"isrs_for_reviewer_with_user_id_{reviewer.user_id}.csv" 

516 response = Response(status_code=HTTPStatus.OK, body=isr_csv.encode()) 

517 response.headers["Content-Disposition"] = "attachment;filename=" + filename 

518 response.headers["Access-Control-Expose-Headers"] = "Content-Disposition" 

519 return response 

520 

521 

522@view_config(route_name="individual_science_review_import", permission="individual_science_review_import") 

523def individual_science_review_import(request: Request) -> Response: 

524 """Import updates to a ScienceReviewer's IndividualScienceReviews from a CSV file 

525 

526 URL: science_reviewer/{reviewer_id}/import_isrs 

527 

528 :param request: POST request with the ScienceReviewer's ID 

529 :return: A response with the given ScienceReviewer's ISRs imported from the provided CSV file, 

530 or 401 response HTTPUnauthorized if the requesting User isn't either the given ScienceReviewer's User 

531 or a TTA member 

532 or 404 response (HTTPNotFound) if no Reviewer exists with an ID of reviewer_id 

533 or 400 response (HTTPBadRequest) if reviewer_id is not an integer 

534 or if no file was uploaded 

535 or if file couldn't be parsed as a CSV 

536 or if errors found when attempting to import 

537 """ 

538 is_tta_member = bool(request.identity) and "tta_member" in request.identity.roles 

539 

540 reviewer: ScienceReviewer = request.lookup(request.matchdict["reviewer_id"], ScienceReviewer) 

541 

542 if not is_tta_member: # verify that the reviewer is attached to the user requesting the action: 

543 if reviewer.user_id != request.identity.user_id: 

544 raise HTTPUnauthorized( 

545 body=f"User {request.identity.user_id} is not authorized to import individual science reviews " 

546 f"for {reviewer.user_id}" 

547 ) 

548 

549 if "isr_csv_import" not in request.POST: 

550 raise HTTPBadRequest("No file was uploaded") 

551 

552 import_file = request.POST["isr_csv_import"].file.read() 

553 

554 isrs_to_update, error_messages = deserialize_csv_to_isrs(import_file, reviewer, is_tta_member, request.repo) 

555 

556 if len(error_messages): 

557 errors = f"CSV errors found when attempting to import Individual Science Reviews: {', '.join(error_messages)}" 

558 raise HTTPBadRequest(body=errors) 

559 

560 # No errors found during parsing - persist the updates to the ISRs that were returned 

561 for isr in isrs_to_update: 

562 request.repo.individual_science_review_repo.update(isr) 

563 

564 # Return all the ISRs for this reviewer 

565 all_isrs_for_reviewer = request.repo.individual_science_review_repo.list_by_reviewer_id( 

566 reviewer.science_reviewer_id 

567 ) 

568 json = [isr.__json__() for isr in all_isrs_for_reviewer] 

569 

570 return Response(json=json) 

571 

572 

573@view_config( 

574 route_name="individual_science_review_list_by_solicitation", 

575 renderer="json", 

576 permission="individual_science_review_list", 

577) 

578def individual_science_review_list(request: Request) -> Response: 

579 """List the IndividualScienceReviews that are accessible to the requesting user for the specified Solicitation 

580 

581 URL: solicitations/{solicitation_id}/individual_science_reviews 

582 

583 :param request: GET request 

584 :return: Response with JSON-formatted array of IndividualScienceReviews 

585 or 401 response (HTTPUnauthorized) if no userId parameter received 

586 or 400 response (HTTPBadRequest) if the JWT's user_id isn't an integer or isrs for the solicitation could not 

587 be retrieved, or the requester is not a TTAMember or ScienceReviewer 

588 """ 

589 identity = request.identity 

590 if hasattr(identity, "user_id"): 

591 user_id = identity.user_id 

592 if user_id is None: 

593 raise HTTPUnauthorized(body=f"No userId parameter received.") 

594 user = get_user_by_id(user_id) 

595 is_tta_member = True if "tta_member" in user.roles else False 

596 

597 solicitation_id = request.matchdict["solicitation_id"] 

598 

599 try: 

600 isrs: list[IndividualScienceReview] = request.repo.individual_science_review_repo.list_by_solicitation_id( 

601 solicitation_id 

602 ) 

603 except ValueError as e: 

604 raise HTTPBadRequest(body=str(e)) 

605 

606 returned_isrs = [] 

607 

608 sr: ScienceReviewer = request.repo.science_reviewer_repo.by_user_id_and_sol_id(user.user_id, solicitation_id) 

609 

610 is_srp_in_consensus_phase = False # Not relevant unless SR is not None 

611 if is_tta_member: # Return all ISRs for the solicitation 

612 returned_isrs = isrs 

613 # If SR, then determine what kind of reviewer and return appropriate ISRs, 

614 # otherwise return an empty list of ISRs 

615 elif sr is not None: 

616 srp: ScienceReviewPanel = request.repo.science_review_panel_repo.by_id(sr.science_review_panel_id) 

617 is_srp_in_consensus_phase = is_in_consensus_phase(srp, request.repo) 

618 if sr.is_chair or is_srp_in_consensus_phase: 

619 returned_isrs = request.repo.individual_science_review_repo.list_by_srp_id(sr.science_review_panel_id) 

620 """ 

621 else: 

622 for isr in isrs: 

623 if isr.science_reviewer_id == sr.science_reviewer_id: # SR has been assigned the ISR 

624 returned_isrs.append(isr) 

625 """ 

626 

627 # Check for additional ISRs for this reviewer (including external ISRs) 

628 reviewer_isrs = request.repo.individual_science_review_repo.list_by_reviewer_id(sr.science_reviewer_id) 

629 for rev_isr in reviewer_isrs: 

630 if not any( 

631 rev_isr.individual_science_review_id == isr.individual_science_review_id for isr in returned_isrs 

632 ): 

633 returned_isrs.append(rev_isr) 

634 try: 

635 json = anonymize_isrs( 

636 request.repo, returned_isrs, sr, is_tta_member, is_consensus_phase=is_srp_in_consensus_phase 

637 ) 

638 except ValueError as e: 

639 raise HTTPBadRequest(body=str(e)) 

640 return Response(status_code=HTTPStatus.OK, json_body=json)