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

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 

18 

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 

25 

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 

32 

33 

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 

37 

38 URL: obspec_disposition_update/{allocation_disposition_id} with JSON body with list of observation specification 

39 dispositions 

40 

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" 

48 

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 

56 

57 The json returned, proprietaryPeriodDuration is returned in a similar format if the 

58 

59 :param request: PUT Request 

60 

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 """ 

72 

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 ] 

106 

107 is_tta_member = True if "tta_member" in request.identity.roles else False 

108 

109 # Verify that an Allocation Disposition exists with given ID 

110 ad: AllocationDisposition = request.lookup(request.matchdict["allocation_disposition_id"], AllocationDisposition) 

111 

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") 

115 

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 ) 

122 

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 = [] 

128 

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())) 

133 

134 osd = osds[param["observationSpecificationDispositionId"]] 

135 

136 osd.scheduling_priority_name = param["schedulingPriorityName"] 

137 

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) 

151 

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 

179 

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"]) 

188 

189 osd.overhead = Quantity(param["overhead"], second) 

190 

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"]) 

195 

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() 

205 

206 osd.requires_manual_generation = param["requiresManualGeneration"] # this will be checked and set in the FE. 

207 

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}.") 

218 

219 updated_osds.append(osd) 

220 

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) 

224 

225 request.repo.allocation_disposition_repo.update(ad) 

226 

227 return Response(json_body=ad.__json__())