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

61 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 

18import json 

19from datetime import datetime, timezone 

20from random import choice, seed 

21 

22from sqlalchemy.orm import selectinload 

23 

24from allocate.domain_layer.entities.proposal_disposition import ProposalDisposition 

25from auth.auth import get_non_tta_members 

26from common import get_middle_layer 

27from propose.application_layer.services.proposal_state_change_service import create_osr_proposal_review 

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

29from solicit.domain_layer.entities.solicitation import Solicitation 

30from testdata.application_layer.services.allocation_request_generator import generate_allocation_requests 

31from testdata.application_layer.services.proposal_generator_abc import ProposalGenerator 

32 

33 

34class TestProposalGenerator(ProposalGenerator): 

35 def __init__(self, repo): 

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() / f"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 

45 def generate_proposals( 

46 self, 

47 solicitation: Solicitation, 

48 do_submit: bool, 

49 num_proposals=8, 

50 make_obspecs=True, 

51 random_seed=None, 

52 ) -> list[Proposal]: 

53 if random_seed: 

54 seed(random_seed) # set the given random seed 

55 abstract = "Lorem ipsum" 

56 proposals = [] 

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

58 num_existing_props = len( 

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

60 ) 

61 for i in range(0, num_proposals): 

62 title = f"Simulated Proposal {num_existing_props + i} for Test Solicitation" 

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

64 if do_submit: 

65 proposal_number = str(solicitation.current_suffix) 

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

67 solicitation.current_suffix = solicitation.current_suffix + 1 

68 else: 

69 proposal_code = None 

70 proposals.append( 

71 self.generate_proposal( 

72 title=title, 

73 abstract=abstract, 

74 solicitation=solicitation, 

75 make_obspecs=make_obspecs, 

76 do_submit=do_submit, 

77 proposal_code=proposal_code, 

78 ) 

79 ) 

80 return proposals 

81 

82 def generate_proposal( 

83 self, 

84 title: str, 

85 abstract: str, 

86 solicitation: Solicitation, 

87 make_obspecs: bool, 

88 do_submit: bool, 

89 proposal_code: str, 

90 ) -> Proposal: 

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

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

93 prop.is_triggered = choice([True, False]) 

94 

95 user = choice(get_non_tta_members()) 

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

97 prop.authors = [author] 

98 

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

100 

101 prop.author_copy = ProposalCopy( 

102 title, 

103 abstract, 

104 self.pdf, 

105 science_category=choice(solicitation.science_categories), 

106 proposal=prop, 

107 ) 

108 

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

110 

111 prop.author_copy.proposal_class = choice(solicitation.proposal_process.proposal_classes) 

112 if do_submit: 

113 prop.observatory_copy = ProposalCopy( 

114 title, 

115 abstract, 

116 self.pdf, 

117 science_category=prop.author_copy.science_category, 

118 proposal=prop, 

119 ) 

120 prop.observatory_copy.proposal_class = prop.author_copy.proposal_class 

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

122 prop.observatory_copy_id = self.repo.proposal_copy_repo.add(prop.observatory_copy) 

123 

124 # make ARs, CRs, and obspecs for prop 

125 generate_allocation_requests( 

126 repo=self.repo, 

127 proposal=prop, 

128 test_type="Test", 

129 make_obspecs=make_obspecs, 

130 do_submit=do_submit, 

131 all_scan_intents=self.all_scan_intents, 

132 all_subscan_intents=self.all_subscan_intents, 

133 calibrator_list=self.calibrator_list, 

134 sfcs=solicitation.solicitation_facility_capabilities, 

135 ) 

136 

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

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

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

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

141 prop.state = "In Review" 

142 create_osr_proposal_review(prop, self.repo) 

143 try: 

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

145 except ValueError as e: 

146 pd = ProposalDisposition(prop) 

147 self.repo.proposal_disposition_repo.add(pd) 

148 prop.vetted_science_category = prop.author_copy.science_category 

149 

150 return prop