Coverage for middle_layer/allocate/application_layer/rest_api/views/observation_specification_disposition.py: 16.85%
89 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/>.
17from datetime import datetime
19from astropy import units as u
20from astropy.units import Quantity, second
21from pyramid.httpexceptions import HTTPBadRequest, HTTPUnauthorized
22from pyramid.request import Request
23from pyramid.response import Response
24from pyramid.view import view_config
26from allocate.domain_layer.entities.allocation_disposition import AllocationDisposition
27from allocate.domain_layer.entities.observation_specification_disposition import ProprietaryPeriodType
28from allocate.domain_layer.entities.temporal_reference import TemporalReference
29from allocate.domain_layer.services.validate_osd_service import validate_osd
30from common.application_layer.rest_api import make_expected_params_message
31from common.utils.duration import convert_to_timedelta
34@view_config(route_name="obspec_disposition_update", renderer="json", permission="obspec_disposition_update")
35def obspec_disposition_update(request: Request) -> Response:
36 """Update a given Observation Specification Disposition
38 URL: obspec_disposition_update/{allocation_disposition_id} with JSON body with list of observation specification
39 dispositions
41 Note regarding proprietaryPeriod paramaters:
42 proprietaryPeriodType can be "FROM_FIRST_OBSERVATION", "FROM_LAST_OBSERVATION", "NO_PROPRIETARY_PERIOD", or
43 "PERIOD_ENDS_ON"
44 proprietaryPeriodDate is expected to be an ISO 8601 date string
45 The expected parameter, proprietaryPeriodDuration, is a string in the format "[X]Year(s) [Y]Month(s) [Z]Day(s)"
46 and is returned in
47 the same format"
49 - if FROM_FIRST_OBSERVATION is specified, proprietaryPeriodDuration is taken into account and
50 proprietaryPeriodDate is ignored
51 - if FROM_LAST_OBSERVATION is specified, proprietaryPeriodDuration is taken into account and
52 proprietaryPeriodDate is ignored
53 - if NO_PROPRIETARY_PERIOD is specified, proprietaryPeriodDate and proprietaryPeriodDuration are ignored
54 - if PERIOD_ENDS_ON is specified, proprietaryPeriodDate is taken into account and proprietaryPeriodDuration is
55 ignored
57 The json returned, proprietaryPeriodDuration is returned in a similar format if the
59 :param request: PUT Request
61 :return: Response with the Allocation Disposition's json.
62 or 404 response (HTTPNotFound) if no AllocationDisposition exists with the given id,
63 or any specified facility configurations are malformed
64 or 400 response (HTTPBadRequest) if allocation_disposition_id is not an integer,
65 or the parameter proprietaryPeriodDate is not supplied when the proprietaryPeriodType is PERIOD_ENDS_ON,
66 or the parameter proprietaryPeriodDate is not a valid ISO 8601 date string,
67 or the parameter proprietaryPeriodDuration is not supplied when the proprietaryPeriodType is FROM_FIRST_OBSERVATION or FROM_LAST_OBSERVATION
68 or the parameter proprietaryPeriodDuration is not a valid duration string,
69 or any specified facility configurations are malformed
70 or 401 response (HTTPUnauthorized) if the requesting user is not a TTA member
71 """
73 params = request.json_body
74 expected_params = [
75 "observationSpecificationDispositionId",
76 "allocationRequestId",
77 "allocationDispositionId",
78 "schedulingPriorityName",
79 "totalApprovedTime",
80 "overhead",
81 "allocatedTimePerRepeat",
82 "allocatedEarliestStartTime",
83 "allocatedLatestEndTime",
84 "allocatedEarliestDate",
85 "allocatedLatestDate",
86 "allocatedCadence",
87 "requestedCadence",
88 "isRequestedFiller",
89 "isProposerModified",
90 "temporalReference",
91 "requestedHardwareConfiguration",
92 "allocatedHardwareConfiguration",
93 "durationPerRepeat",
94 "totalDuration",
95 "scans",
96 "schedulingPriorityLocked",
97 "requiresManualGeneration",
98 "requestedEarliestStartTime",
99 "requestedLatestEndTime",
100 "totalScienceTargetIntegrationTime",
101 "totalTimeOnObservingTarget",
102 "proprietaryPeriodType",
103 "proprietaryPeriodDate",
104 "proprietaryPeriodDuration",
105 ]
107 is_tta_member = True if "tta_member" in request.identity.roles else False
109 # Verify that an Allocation Disposition exists with given ID
110 ad: AllocationDisposition = request.lookup(request.matchdict["allocation_disposition_id"], AllocationDisposition)
112 # User making update request must be TTA member
113 if is_tta_member is False:
114 raise HTTPUnauthorized(body=f"User {request.identity.user_id} is not a TTA Member")
116 # version cannot be read-only:
117 if ad.allocation_version.is_read_only:
118 raise HTTPBadRequest(
119 body=f"Allocation Version {ad.allocation_version.allocation_version_id} "
120 f"({ad.allocation_version.number}) is read-only and cannot be modified."
121 )
123 # make a mapping from ID to object, which we'll need shortly to make the updates
124 osds = dict(
125 (osd.observation_specification_disposition_id, osd) for osd in ad.observation_specification_dispositions
126 )
127 updated_osds = []
129 for param in params:
130 # JSON params do not contain all expected params
131 if not all([expected in param for expected in expected_params]):
132 raise HTTPBadRequest(body=make_expected_params_message(expected_params, param.keys()))
134 osd = osds[param["observationSpecificationDispositionId"]]
136 osd.scheduling_priority_name = param["schedulingPriorityName"]
138 osd.proprietary_period_type = ProprietaryPeriodType(param["proprietaryPeriodType"])
139 # create cadence entities from params
140 for cadence_type in "requested", "allocated":
141 if param[f"{cadence_type}Cadence"]:
142 repeat_counts = [int(d) for d in param[f"{cadence_type}Cadence"]["repeatCounts"]]
143 deltas = [Quantity(d, u.s) for d in param[f"{cadence_type}Cadence"]["deltas"]]
144 tolerances = [Quantity(t, u.s) for t in param[f"{cadence_type}Cadence"]["tolerances"]]
145 the_cadence = osd.requested_cadence if cadence_type == "requested" else osd.allocated_cadence
146 the_cadence.repeat_counts = repeat_counts
147 the_cadence.deltas = deltas
148 the_cadence.tolerances = tolerances
149 # update the cadence on the OSD
150 setattr(osd, f"{cadence_type}_cadence", the_cadence)
152 if osd.proprietary_period_type == ProprietaryPeriodType.NO_PROPRIETARY_PERIOD:
153 osd.proprietary_period_date = None
154 osd.proprietary_period_duration = None
155 elif osd.proprietary_period_type == ProprietaryPeriodType.PERIOD_ENDS_ON:
156 if "proprietaryPeriodDate" not in param:
157 raise HTTPBadRequest(
158 body=f"proprietaryPeriodDate is required when proprietaryPeriodType is "
159 f"{osd.proprietary_period_type.value}"
160 )
161 try:
162 datetime.fromisoformat(param["proprietaryPeriodDate"])
163 # if ValueError or TypeError is raised, the date string is invalid
164 # and we'll raise a 400
165 except (ValueError, TypeError):
166 raise HTTPBadRequest(
167 body=f"proprietaryPeriodDate must be an ISO 8601 date string, e.g. "
168 f"2023-01-01. Received {param['proprietaryPeriodDate']}"
169 )
170 osd.proprietary_period_date = datetime.fromisoformat(param["proprietaryPeriodDate"]).date()
171 osd.proprietary_period_duration = None
172 else: # FROM_FIRST_OBSERVATION or FROM_LAST_OBSERVATION
173 if "proprietaryPeriodDuration" not in param:
174 raise HTTPBadRequest(
175 body=f"proprietaryPeriodDuration is required when proprietaryPeriodType is "
176 f"{osd.proprietary_period_type.value}"
177 )
178 osd.proprietary_period_date = None
180 try:
181 osd.proprietary_period_duration = convert_to_timedelta(param["proprietaryPeriodDuration"])
182 except (AttributeError, ValueError):
183 raise HTTPBadRequest(
184 body=f"proprietaryPeriodDuration must be a duration string, e.g. 1 year 3 months 2 days. Received "
185 f"{param['proprietaryPeriodDuration']}"
186 )
187 osd.proprietary_period_duration = convert_to_timedelta(param["proprietaryPeriodDuration"])
189 osd.overhead = Quantity(param["overhead"], second)
191 osd.is_requested_filler = param["isRequestedFiller"]
192 osd.is_proposer_modified = param["isProposerModified"]
193 osd.scheduling_priority_locked = param["schedulingPriorityLocked"]
194 osd.temporal_reference = TemporalReference(param["temporalReference"])
196 new_allocated_time_per_repeat = Quantity(param["allocatedTimePerRepeat"], second)
197 new_allocated_earliest_start_time = Quantity(param["allocatedEarliestStartTime"], second)
198 new_allocated_latest_end_time = Quantity(param["allocatedLatestEndTime"], second)
199 new_allocated_earliest_date = None
200 if param["allocatedEarliestDate"]:
201 new_allocated_earliest_date = datetime.fromisoformat(param["allocatedEarliestDate"]).date()
202 new_allocated_latest_date = None
203 if param["allocatedLatestDate"]:
204 new_allocated_latest_date = datetime.fromisoformat(param["allocatedLatestDate"]).date()
206 osd.requires_manual_generation = param["requiresManualGeneration"] # this will be checked and set in the FE.
208 osd.allocated_time_per_repeat = new_allocated_time_per_repeat
209 # Allocated Solicitation
210 osd.allocated_earliest_start_time = new_allocated_earliest_start_time
211 osd.allocated_latest_end_time = new_allocated_latest_end_time
212 osd.allocated_earliest_date = new_allocated_earliest_date
213 osd.allocated_latest_date = new_allocated_latest_date
214 # validate new osd:
215 errors = validate_osd(osd)
216 if errors != "":
217 raise HTTPBadRequest(body=f"OSD could not be updated: {errors}.")
219 updated_osds.append(osd)
221 # this will cause any that didn't get updated to be deleted, which I think is desired
222 ad.observation_specification_dispositions.clear()
223 ad.observation_specification_dispositions.extend(updated_osds)
225 request.repo.allocation_disposition_repo.update(ad)
227 return Response(json_body=ad.__json__())