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
« 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/>.
18import json
19from datetime import datetime, timezone
21from sqlalchemy.orm import selectinload
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
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
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
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 )
83 return proposals
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])
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
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]
104 prop.proposal_id = self.repo.proposal_repo.add(prop)
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 )
116 prop.author_copy.proposal_copy_id = self.repo.proposal_copy_repo.add(prop.author_copy)
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 )
142 # generate answers for techjust questions:
143 answer_techjust_questions(prop.author_copy, self.repo)
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
158 return prop