Coverage for middle_layer/testdata/application_layer/services/proposal_generator_deterministic.py: 100.00%

62 statements  

« prev     ^ index     » next       coverage.py v7.10.5, created at 2026-04-13 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 

18import json 

19from datetime import datetime, timezone 

20 

21from sqlalchemy.orm import selectinload 

22 

23from allocate.domain_layer.entities.proposal_disposition import ProposalDisposition 

24from auth.auth import get_non_tta_members 

25from common import get_middle_layer 

26from propose.application_layer.services.proposal_state_change_service import create_osr_proposal_review 

27from propose.domain_layer.entities.proposal import Author, Proposal, ProposalCopy 

28from solicit.domain_layer.entities.solicitation import Solicitation 

29from testdata.application_layer.services.allocation_request_generator import generate_allocation_requests 

30from testdata.application_layer.services.proposal_generator_abc import ProposalGenerator 

31from testdata.application_layer.services.proposal_generator_demo import answer_techjust_questions 

32 

33 

34class DeterministicProposalGenerator(ProposalGenerator): 

35 def __init__(self, repo, force_num_proposals=False): 

36 self.repo = repo 

37 self.all_scan_intents = {si.name: si for si in repo.scan_intent_repo.list_all()} 

38 self.all_subscan_intents = {ssi.name: ssi for ssi in repo.subscan_intent_repo.list_all()} 

39 with open((get_middle_layer() / "testdata/config/calibratorList.json").resolve()) as f: 

40 self.calibrator_list = json.load(f) 

41 self.pdf = open( 

42 (get_middle_layer() / "propose/application_layer/rest_api/test/files" / "sample1.pdf").resolve(), "rb" 

43 ).read() 

44 self.force_num_proposals = force_num_proposals 

45 

46 def generate_proposals( 

47 self, 

48 solicitation: Solicitation, 

49 do_submit: bool, 

50 num_proposals=10, 

51 make_obspecs=True, 

52 random_seed=None, 

53 ) -> list[Proposal]: 

54 # since this should be deterministic, some parameters are ignored: 

55 num_proposals = 10 if not self.force_num_proposals else num_proposals 

56 abstract = "Lorem ipsum" 

57 proposals = [] 

58 # Determine the current max proposal id to keep titles contiguous 

59 num_existing_props = len( 

60 self.repo.proposal_repo.list_filtered(solicitation_id=solicitation.solicitation_id, state="Submitted") 

61 ) 

62 for i in range(0, num_proposals): 

63 title = f"Simulated Proposal {num_existing_props + i} for Deterministic Solicitation" 

64 # DB handles prop code normally, construct it here if generating submitted proposals 

65 if do_submit: 

66 proposal_number = str(solicitation.current_suffix) 

67 proposal_code = f"{solicitation.proposal_code_prefix}-{proposal_number.zfill(4)}" 

68 solicitation.current_suffix = solicitation.current_suffix + 1 

69 else: 

70 proposal_code = None 

71 

72 proposals.append( 

73 self.generate_proposal( 

74 title=title, 

75 abstract=abstract, 

76 solicitation=solicitation, 

77 make_obspecs=make_obspecs, 

78 do_submit=do_submit, 

79 proposal_code=proposal_code, 

80 ) 

81 ) 

82 

83 return proposals 

84 

85 def generate_proposal( 

86 self, 

87 title: str, 

88 abstract: str, 

89 solicitation: Solicitation, 

90 make_obspecs: bool, 

91 do_submit: bool, 

92 proposal_code: str, 

93 ) -> Proposal: 

94 proposal_num = int(title[19]) 

95 

96 # much of this is copied from DemoProposalGenerator. This will change as the AR/CR requirements change. 

97 prop = Proposal(solicitation, proposal_code=proposal_code, state="Submitted" if do_submit else "Draft") 

98 prop.is_triggered = proposal_num % 4 == 0 # set proposals 0 and 3 to triggered 

99 

100 user = get_non_tta_members()[proposal_num] # set authors to the first 8 non-tta users 

101 author = Author(user.first_name, user.last_name, True, user.user_id, prop) 

102 prop.authors = [author] 

103 

104 prop.proposal_id = self.repo.proposal_repo.add(prop) 

105 

106 prop.author_copy = ProposalCopy( 

107 title, 

108 abstract, 

109 self.pdf, 

110 science_category=solicitation.science_categories[ 

111 proposal_num % 3 

112 ], # rotate through the first 3 science categories 

113 proposal=prop, 

114 ) 

115 

116 prop.author_copy.proposal_copy_id = self.repo.proposal_copy_repo.add(prop.author_copy) 

117 

118 prop.author_copy.proposal_class = solicitation.proposal_process.proposal_classes[0] # set to Regular 

119 if do_submit: 

120 prop.observatory_copy = ProposalCopy( 

121 title, 

122 abstract, 

123 self.pdf, 

124 science_category=prop.author_copy.science_category, 

125 proposal=prop, 

126 ) 

127 prop.observatory_copy.proposal_class = prop.author_copy.proposal_class 

128 prop.submitted_timestamp = datetime.now(timezone.utc) 

129 # make ARs, CRs, and obspecs for prop 

130 generate_allocation_requests( 

131 repo=self.repo, 

132 proposal=prop, 

133 test_type="Deterministic", 

134 make_obspecs=make_obspecs, 

135 do_submit=do_submit, 

136 all_scan_intents=self.all_scan_intents, 

137 all_subscan_intents=self.all_subscan_intents, 

138 calibrator_list=self.calibrator_list, 

139 sfcs=solicitation.solicitation_facility_capabilities, 

140 ) 

141 

142 # generate answers for techjust questions: 

143 answer_techjust_questions(prop.author_copy, self.repo) 

144 

145 if do_submit and solicitation.proposal_process.proposal_process_name == "Observatory Site Review": 

146 # move the proposal forward to 'In Review' and create reviews and disposition for it. 

147 # It's risky to not do this through the state change service, but we avoid it here for efficiency. 

148 # also setting vetted SC here, which is typically handled by state change service 

149 prop.state = "In Review" 

150 create_osr_proposal_review(prop, self.repo) 

151 try: 

152 pd = self.repo.proposal_disposition_repo.by_id(prop.proposal_id) 

153 except ValueError as e: 

154 pd = ProposalDisposition(prop) 

155 self.repo.proposal_disposition_repo.add(pd) 

156 prop.vetted_science_category = prop.author_copy.science_category 

157 

158 return prop