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
« 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
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
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
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,
61 URL: individual_science_reviews
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()
83 tta_member = False
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
89 if request.identity and "tta_member" in request.identity.roles:
90 tta_member = True
92 isrs_to_update = list()
94 if len(params) == 0:
95 return Response(status_code=HTTPStatus.OK, json=[])
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)
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.")
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
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()))
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 )
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 )
138 reviewer_associated_with_isr: ScienceReviewer = request.repo.science_reviewer_repo.by_id(
139 isr.science_reviewer_id
140 )
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 )
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
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
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
176 for isr in isrs_to_update:
177 request.repo.individual_science_review_repo.update(isr)
178 updated_isrs.append(isr)
180 return Response(status_code=HTTPStatus.OK, json=[isr.__json__() for isr in updated_isrs])
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
191 Where an external ISR exists on its Proposal's SRP, and its Proposal and ScienceReviewer belong to different SRPs
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 )
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__())
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
265 URL: individual_science_reviews/{isr_id}
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
286 isr: IndividualScienceReview = request.lookup(request.matchdict["isr_id"], IndividualScienceReview)
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 )
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()))
308 if not is_tta_member and request.identity.user_id != reviewer.user_id:
309 raise HTTPUnauthorized(body=f"Insufficient permission to complete operation.")
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
330 request.repo.individual_science_review_repo.update(isr)
332 return Response(json=isr.__json__())
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
343 URL: individual_science_reviews/finalize/{reviewer_id}
345 Note: If all ISRs for the SRP are discovered to be finalized, a notification is sent to TTA members
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
359 reviewer: ScienceReviewer = request.lookup(request.matchdict["reviewer_id"], ScienceReviewer)
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 )
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))
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)
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))
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]
390 return Response(json=response_json)
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.
401 Intended to be called by the chair during Consensus phase.
403 URL: individual_science_reviews/update_fnis
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
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()))
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")
431 srp_id = next(iter(srp_ids))
432 panel = request.repo.science_review_panel_repo.by_id(srp_id)
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 )
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 )
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 )
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))
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)
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
492 URL: science_reviewer/{reviewer_id}/export_isrs
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)
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 )
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
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
526 URL: science_reviewer/{reviewer_id}/import_isrs
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
540 reviewer: ScienceReviewer = request.lookup(request.matchdict["reviewer_id"], ScienceReviewer)
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 )
549 if "isr_csv_import" not in request.POST:
550 raise HTTPBadRequest("No file was uploaded")
552 import_file = request.POST["isr_csv_import"].file.read()
554 isrs_to_update, error_messages = deserialize_csv_to_isrs(import_file, reviewer, is_tta_member, request.repo)
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)
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)
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]
570 return Response(json=json)
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
581 URL: solicitations/{solicitation_id}/individual_science_reviews
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
597 solicitation_id = request.matchdict["solicitation_id"]
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))
606 returned_isrs = []
608 sr: ScienceReviewer = request.repo.science_reviewer_repo.by_user_id_and_sol_id(user.user_id, solicitation_id)
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 """
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)