Coverage for middle_layer/review/domain_layer/services/finalize_prop_reviews_service.py: 100.00%
25 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/>.
18import numpy
20from common.application_layer.orm_repositories.orm_repository import ORMRepository
21from review.domain_layer.entities.ppr_proposal_review import PPRProposalReview
22from review.domain_layer.entities.science_review_panel import ScienceReviewPanel
23from review.domain_layer.services.validate_ppr_prop_rev_score_service import (
24 PPR_PROP_REV_SCORE_RANGE_MESSAGE,
25 validate_ppr_prop_review_score,
26)
27from review.domain_layer.services.validate_pprpr_state_change_service import validate_pprpr_state_change
30def calculate_normalized_linear_rank_score(scores: list[float], index: int) -> float:
31 """
32 Calculate the Normalized Linear Rank Score for a given element of a list of SRP scores
34 The formula is: 10R/n
35 where R is the ordinal rank of the SRP Score to be processed (in ascending order)
36 and n is the number of proposals reviewed by the Science Review Panel (len(scores) here).
38 :param scores: the list of SRP scores to normalize against, including the one to be processed
39 :param index: the index, in scores, of the SRP score to be processed
40 :return: the Normalized Linear Rank Score for scores[index] given scores, rounded to the nearest tenth.
41 :raises IndexError: When index isn't in [0, len(scores))
42 """
43 R = sorted(scores).index(scores[index]) + 1
44 n = len(scores)
45 nlr_score = 10 * R / n
46 return numpy.round(nlr_score, 1)
49def finalize_ppr_proposal_reviews(
50 srp: ScienceReviewPanel, repo: ORMRepository, is_tta_member: bool = False
51) -> list[PPRProposalReview]:
52 """Validate and finalize all PPRProposalReviews for a given ScienceReviewPanel
54 :param srp: ScienceReviewPanel to finalize PPRProposalReviews for
55 :param repo: Repository to query the database
56 :param is_tta_member: Whether or not the user requesting this state change is a TTA Member
57 :return: List of srp's PPRProposalReviews, finalized
58 :raises ValueError: When srp's PPRProposalReviews can't be finalized
59 """
60 proposal_reviews = repo.ppr_proposal_review_repo.list_by_srp_id(srp.science_review_panel_id)
61 srp_scores = [prop_rev.calculated_srp_score for prop_rev in proposal_reviews]
62 for i, proposal_review in enumerate(proposal_reviews):
63 if not proposal_review.external_science_review_comments:
64 raise ValueError(f"PPRProposalReview for Proposal {proposal_review.proposal_id} has no comment for the PI")
65 if not validate_ppr_prop_review_score(proposal_review.calculated_srp_score):
66 raise ValueError(
67 f"{PPR_PROP_REV_SCORE_RANGE_MESSAGE}, found {proposal_review.calculated_srp_score}"
68 f" for PPRProposalReview of Proposal {proposal_review.proposal_id}"
69 )
70 validation_message = validate_pprpr_state_change(
71 proposal_review.review_state, "Finalized", is_tta_member=is_tta_member
72 )
73 if validation_message:
74 raise ValueError(f"{validation_message} for PPRProposalReview for Proposal {proposal_review.proposal_id}")
75 proposal_review.review_state = "Finalized"
76 proposal_review.scientific_merit_metric = calculate_normalized_linear_rank_score(srp_scores, i)
77 return proposal_reviews