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
« 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 json
19from datetime import datetime, timezone
20from random import choice, seed
22from sqlalchemy.orm import selectinload
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
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()
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
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])
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]
99 prop.proposal_id = self.repo.proposal_repo.add(prop)
101 prop.author_copy = ProposalCopy(
102 title,
103 abstract,
104 self.pdf,
105 science_category=choice(solicitation.science_categories),
106 proposal=prop,
107 )
109 prop.author_copy.proposal_copy_id = self.repo.proposal_copy_repo.add(prop.author_copy)
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)
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 )
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
150 return prop