Coverage for middle_layer/review/domain_layer/services/deserialize_csv_to_isrs_service.py: 96.43%

56 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"""Deserialize CSV as IndividualScienceReviews to be imported 

18 

19The following columns allow the middle layer to update the given ISRs when imported. 

20They are: 

21 username - With Solicitation, identifies the ScienceReviewer who wrote the ISR via user_id 

22 proposal_id (proposal_code) - Identifies a Proposal and Solicitation. 

23 comments_for_the_srp - comments to update 

24 individual_score - score to update (must be between 0.1 and 9.9 in increments of a tenth) 

25 

26All other attributes of ISRs or related objects can't be edited by ScienceReviewers, 

27 aren't meaningful to users, 

28 or aren't needed to identify an ISR. 

29""" 

30 

31import csv 

32from io import BytesIO, StringIO 

33 

34from auth.auth import get_user_by_username 

35from common.application_layer.orm_repositories.orm_repository import ORMRepository 

36from common.domain_layer.services.csv_service import compile_error_messages, get_rows 

37from review.domain_layer.entities.individual_science_review import IndividualScienceReview 

38from review.domain_layer.entities.science_reviewer import ScienceReviewer 

39from review.domain_layer.services.validate_isr_score_service import ( 

40 INDIVIDUAL_REV_SCORE_RANGE_MESSAGE, 

41 validate_isr_score, 

42) 

43from review.domain_layer.services.validate_isr_state_change_service import validate_isr_state_change 

44 

45 

46def deserialize_csv_to_isrs( 

47 file_content: bytes, reviewer: ScienceReviewer, is_tta_member: bool, repo: ORMRepository 

48) -> tuple[list[IndividualScienceReview], set[str]]: 

49 """Deserialize a given CSV file, updating the IndividualScienceReviews specified in it 

50 

51 Note: the caller is responsible for persisting updates to the IndividualScienceReviews being returned, if so desired 

52 

53 :param file_content: CSV file contents 

54 :param reviewer: the ScienceReviewer that the import is being performed on behalf of 

55 :param is_tta_member: whether or not a TTA member is making the request on behalf of a ScienceReviewer 

56 :param repo: Repository to query the database 

57 :return: Tuple consisting of either: 

58 a list of ISRs containing updates parsed from file_content, plus an empty set, if parsing succeeded; 

59 or an empty list and a set of errors found during parsing if it failed 

60 """ 

61 

62 rows, error_messages = get_rows(file_content) 

63 isrs_to_return = [] 

64 if error_messages: 

65 return isrs_to_return, error_messages 

66 

67 # Check for required columns 

68 required_columns = ["username", "proposal_id", "comments_for_the_srp", "individual_score"] 

69 headers = rows[0].keys() 

70 for column in required_columns: 

71 if column not in headers: 

72 error_messages = compile_error_messages( 

73 error_messages, f"Required column {column} not found in CSV file", abort=True 

74 ) 

75 if len(error_messages): 

76 return isrs_to_return, error_messages 

77 

78 # Process the data contained in the CSV 

79 isr_update_data = [] 

80 for i, row in enumerate(rows): 

81 # Verify the username exists and that the user is also the reviewer 

82 try: 

83 row_reviewer = get_user_by_username(row["username"]) 

84 if row_reviewer.user_id != reviewer.user_id: 

85 error_messages = compile_error_messages( 

86 error_messages, f"Row {i}: Username {row['username']} does " f"not match that of the reviewer" 

87 ) 

88 except ValueError: 

89 error_messages = compile_error_messages(error_messages, f"Row {i}: Username {row['username']} not found") 

90 

91 # Verify that the specified proposal is associated with an ISR and this reviewer 

92 science_review = None 

93 

94 # Note - CSV references proposal_id which is user-facing. Internally, this is proposal_code 

95 science_review = repo.individual_science_review_repo.by_reviewer_id_and_proposal_code( 

96 reviewer.science_reviewer_id, row["proposal_id"] 

97 ) 

98 if not science_review: 

99 error_messages = compile_error_messages( 

100 error_messages, f"Row {i}: Proposal_id {row['proposal_id']} not found" 

101 ) 

102 

103 # Verify that there is a comment 

104 if not len(row["comments_for_the_srp"]): 

105 error_messages = compile_error_messages(error_messages, f"Row {i}: No comment for the SRP found") 

106 

107 # Verify that there is a comment 

108 if not validate_isr_score(float(row["individual_score"])): 

109 error_messages = compile_error_messages( 

110 error_messages, 

111 f"Row {i}: invalid individual score found - {INDIVIDUAL_REV_SCORE_RANGE_MESSAGE}", 

112 ) 

113 

114 if science_review: 

115 row["individual_science_review_id"] = science_review.individual_science_review_id 

116 

117 isr_update_data.append(row) 

118 

119 # If no errors found, verify that all ISRs can be updated to a Saved review state 

120 if not len(error_messages): 

121 for i, isr_update in enumerate(isr_update_data): 

122 # Get the ISR to check if the review state can be changed to Saved 

123 target_isr = repo.individual_science_review_repo.by_id(isr_update["individual_science_review_id"]) 

124 if not is_tta_member and target_isr.conflict_declaration.conflict_state != "Available": 

125 x = isr_update_data.pop(i) # Don't update the ISR if conflicted 

126 else: 

127 # Confirm that the ISR can be updated to a Saved review state 

128 validation_message = validate_isr_state_change( 

129 target_isr.review_state, "Saved", is_tta_member, is_finalize_call=False 

130 ) 

131 if validation_message: 

132 error_messages = compile_error_messages( 

133 error_messages, 

134 f"Invalid IndividualScienceReview state transition requested: {target_isr.review_state}->" 

135 f"Saved for Proposal_id {row['proposal_id']}", 

136 ) 

137 # If still no errors found, ready the specific ISRs for update 

138 if not len(error_messages): 

139 for isr_update in isr_update_data: 

140 # Get the ISR to update 

141 target_isr = repo.individual_science_review_repo.by_id(isr_update["individual_science_review_id"]) 

142 target_isr.comments_for_the_srp = isr_update["comments_for_the_srp"] 

143 target_isr.individual_score = float(isr_update["individual_score"]) 

144 target_isr.review_state = "Saved" 

145 isrs_to_return.append(target_isr) 

146 

147 return isrs_to_return, error_messages