Coverage for middle_layer/testdata/application_layer/rest_api/views/testdata.py: 69.59%

296 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 http import HTTPStatus 

20 

21from pyramid.httpexceptions import HTTPBadRequest, HTTPInternalServerError 

22from pyramid.request import Request 

23from pyramid.response import Response 

24from pyramid.view import view_config 

25from sqlalchemy.orm.session import Session 

26 

27from allocate.domain_layer.entities.proposal_disposition_group import ProposalDispositionGroup 

28from allocate.domain_layer.entities.publication_destination import PublicationDestination 

29from common import get_middle_layer 

30from common.application_layer.rest_api import make_expected_params_message 

31from review.domain_layer.entities.science_review_panel import ScienceReviewPanel 

32from solicit.domain_layer.entities.solicitation import ProposalProcess, Solicitation 

33from testdata.application_layer.services.context import Context 

34 

35 

36def get_context(session: Session, solicitation_id: int) -> Context: 

37 try: 

38 return Context(session, solicitation_id) 

39 except AssertionError: 

40 raise HTTPBadRequest(f"Requested Solicitation with ID={solicitation_id} is not a Test or Demo Solicitation.") 

41 

42 

43@view_config( 

44 route_name="read_context", 

45 renderer="json", 

46 permission="set_context", 

47) 

48def read_context(request: Request) -> Response: 

49 """ 

50 URL: testdata/{solicitation_id}/context 

51 

52 :param request: GET request 

53 :return: 200 response (Success) with json_body of requested Context entity 

54 or 400 response (HTTPBadRequest) if a Solicitation is not a Test or Demo 

55 """ 

56 sol_id = request.matchdict["solicitation_id"] 

57 context = get_context(request.repo.session, sol_id) 

58 return Response(json_body=context.context.__json__(), status_code=HTTPStatus.OK) 

59 

60 

61@view_config( 

62 route_name="set_context", 

63 renderer="json", 

64 permission="set_context", 

65) 

66def set_context(request: Request) -> Response: 

67 """ 

68 Sets context's do_notify field to determine if emails should be sent 

69 

70 URL: testdata/{solicitation_id}/context 

71 

72 :param request: PUT request with a JSON object like: 

73 { 

74 doNotify: <bool> 

75 } 

76 :return: 200 response (Success) with json_body of requested Context entity 

77 or 400 response (HTTPBadRequest) if a Solicitation is not a Test or Demo 

78 or parameters are missing 

79 """ 

80 sol_id = request.matchdict["solicitation_id"] 

81 params = request.json_body 

82 if not "doNotify" in params: 

83 raise HTTPBadRequest(body=make_expected_params_message(["doNotify"], params.keys())) 

84 context = get_context(request.repo.session, sol_id) 

85 context.context.do_notify = params["doNotify"] 

86 

87 

88@view_config( 

89 route_name="generate_proposals", 

90 renderer="json", 

91 permission="generate_proposals", 

92) 

93def generate_proposals(request: Request) -> Response: 

94 """ 

95 Generate and submit Proposals for a testdata Solicitation 

96 URL: solicitations/generate_proposals 

97 

98 :param request: POST request with a JSON object like: 

99 { 

100 solicitationId: <int> 

101 proposalCount: <int> 

102 } 

103 :return: 201 response (Created) with JSON body like: 

104 { 

105 "proposals": [<proposal-json>,...] 

106 } 

107 or 400 response (HTTPBadRequest) if a expected parameters aren't present, 

108 or the Proposal Process ID isn't an int or the Solicitation is not a Test or Demo 

109 or 500 response (HTTPInternalServerError) if a Proposal submission notification fails to send 

110 """ 

111 expected_params = ["solicitationId", "proposalCount"] 

112 params = request.json_body 

113 if not all([expected in params for expected in expected_params]): 

114 # JSON params do not contain all expected params 

115 raise HTTPBadRequest(body=make_expected_params_message(expected_params, params.keys())) 

116 context = get_context(request.repo.session, params["solicitationId"]) 

117 try: 

118 props = context.make_proposals(params["proposalCount"], do_submit=True) 

119 except RuntimeError as e: 

120 raise HTTPInternalServerError(body=str(e)) 

121 return Response(json_body=[prop.__json__() for prop in props], status_code=HTTPStatus.CREATED) 

122 

123 

124@view_config( 

125 route_name="generate_solicitation", 

126 renderer="json", 

127 permission="generate_solicitation", 

128) 

129def generate_solicitation(request: Request) -> Response: 

130 """ 

131 Generate a Solicitation based on the Sem_25A config file 

132 URL: solicitations/generate_solicitation 

133 

134 :param request: POST request with a JSON object like: 

135 { 

136 name: <str>, 

137 proposalCodePrefix: <str>, 

138 proposalProcess: {proposalProcessId: <int>}, 

139 randomSeed: <int, optional> 

140 isDemo: <boolean> 

141 } 

142 :return: 201 response (Created) with JSON body like: 

143 { 

144 "solicitation": <solicitation-json>, 

145 } 

146 or 400 response (HTTPBadRequest) if a expected parameters aren't present, 

147 or the Proposal Process ID isn't an int 

148 or 404 response (HTTPNotFound) if a ProposalProcess with the given proposalProcessId cannot be found 

149 or 500 response (HTTPInternalServerError) if a Proposal submission notification fails to send 

150 """ 

151 expected_params = ["name", "proposalCodePrefix", "proposalProcess", "randomSeed", "isDemo", "doNotify"] 

152 params = request.json_body 

153 if not all([expected in params for expected in expected_params]): 

154 # JSON params do not contain all expected params 

155 raise HTTPBadRequest(body=make_expected_params_message(expected_params, params.keys())) 

156 if "proposalProcessId" not in params["proposalProcess"]: 

157 # JSON params do not contain all expected params 

158 raise HTTPBadRequest(body=make_expected_params_message(["proposalProcessId"], params["proposalProcess"].keys())) 

159 

160 pp = request.lookup(params["proposalProcess"]["proposalProcessId"], ProposalProcess) 

161 

162 with open(get_middle_layer() / "config_files" / "sem26A.json") as file: 

163 sem26_config = json.load(file) 

164 

165 test_type = "Demo" if params["isDemo"] is True else "Test" 

166 

167 try: 

168 context = Context(request.repo.session) 

169 sol = context.make_solicitation( 

170 test_type, 

171 sem26_config, 

172 params["name"], 

173 params["proposalCodePrefix"], 

174 pp, 

175 params["randomSeed"], 

176 params["doNotify"], 

177 ) 

178 except RuntimeError as e: 

179 raise HTTPInternalServerError(body=str(e)) 

180 response = {"solicitation": sol.__json__()} 

181 return Response(json_body=response, status_code=HTTPStatus.CREATED) 

182 

183 

184@view_config( 

185 route_name="generate_science_review_panels", 

186 renderer="json", 

187 permission="science_review_panel_upsert", 

188) 

189def generate_science_review_panels(request: Request) -> Response: 

190 """ 

191 Generate a ScienceReviewPanel for the given test/demo solicitation, with reviewers, and assign proposals 

192 URL: testdata/generate_science_review_panels 

193 

194 :param request: POST request 

195 :return: 201 response (Created) Response with a list of JSON-formatted SRPs 

196 or 400 response (HTTPBadRequest) if expected solicitation_id is not given, 

197 or reference solicitation is not test/demo 

198 or there are unvetted proposals 

199 or there are insufficient available users to assign to a panel 

200 or 404 response (HTTPNotFound) if no solicitation exists with the given solicitation_id 

201 or 412 response (HTTPPreconditionFailed) if there's an issue with persistence or data integrity 

202 """ 

203 solicitation = request.lookup(request.matchdict["solicitation_id"], Solicitation) 

204 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

205 

206 try: 

207 context.make_srps() 

208 except ValueError as e: 

209 raise HTTPBadRequest(body=str(e)) 

210 srps = context.get_srps() 

211 

212 return Response(json_body=[srp.__json__() for srp in srps], status_code=HTTPStatus.CREATED) 

213 

214 

215@view_config( 

216 route_name="vet_all_proposals", 

217 renderer="json", 

218 permission="vet_all_proposals", 

219) 

220def vet_all_proposals(request: Request) -> Response: 

221 """ 

222 Vet all proposals on a test/demo Solicitation 

223 URL: testdata/vet_all_proposals/{solicitation_id} 

224 :return: Response with JSON-formatted vetted proposals 

225 or 400 response (HTTPBadRequest) if the ID is invalid 

226 or 404 response (HTTPNotFound) if no solicitation is found with the ID 

227 """ 

228 solicitation = request.lookup(request.matchdict["solicitation_id"], Solicitation) 

229 context = Context(request.repo.session, solicitation.solicitation_id) 

230 context.vet_all_proposals() 

231 proposals = context.get_proposals() 

232 return Response(status_code=HTTPStatus.OK, json_body=[p.__json__() for p in proposals]) 

233 

234 

235@view_config( 

236 route_name="complete_panel_configuration", 

237 renderer="json", 

238 permission="complete_panel_configuration", 

239) 

240def complete_panel_configuration(request: Request) -> Response: 

241 """ 

242 Complete science panel configuration on a test/demo Solicitation 

243 URL: testdata/complete_panel_configuration/{solicitation_id} 

244 :return: Response with JSON-formatted science review panels 

245 or 400 response (HTTPBadRequest) if the ID is invalid 

246 or if panels have already been created (determined by the presence of ISRs) 

247 or 404 response (HTTPNotFound) if no solicitation is found with the ID 

248 """ 

249 solicitation = request.lookup(request.matchdict["solicitation_id"], Solicitation) 

250 context = Context(request.repo.session, solicitation.solicitation_id) 

251 

252 try: 

253 context.complete_panel_configuration() 

254 except ValueError as e: 

255 raise HTTPBadRequest(body=str(e)) 

256 

257 srps = context.get_srps() 

258 return Response(status_code=HTTPStatus.OK, json_body=[srp.__json__() for srp in srps]) 

259 

260 

261@view_config( 

262 route_name="set_certify_conflicts", 

263 renderer="json", 

264 permission="set_certify_conflicts", 

265) 

266def set_certify_conflicts(request: Request) -> Response: 

267 """ 

268 For the specified ScienceReviewPanel, set all conflict states to Available unless they are AutoConflicted 

269 and certify the conflicts 

270 URL: testdata/set_certify_conflicts 

271 

272 :param request: PUT request 

273 :return: 200 response (Success) Response with a list of JSON-formatted ISRs 

274 or 400 response (HTTPBadRequest) if expected srp_id is not given, or reference solicitation is not test/demo 

275 or 404 response (HTTPNotFound) if no scienceReviewPanel exists with the given srp_id or no solicitation 

276 exists for the srp specified 

277 """ 

278 srp = request.lookup(request.matchdict["srp_id"], ScienceReviewPanel) 

279 solicitation = request.lookup(srp.solicitation_id, Solicitation) 

280 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

281 context.set_certify_conflicts(srp) 

282 isrs = context.get_isrs(srp) 

283 

284 return Response(json_body=[isr.__json__() for isr in isrs], status_code=HTTPStatus.OK) 

285 

286 

287@view_config( 

288 route_name="set_isr_comments_scores", 

289 renderer="json", 

290 permission="individual_science_review_update", 

291) 

292def set_isr_comments_scores(request: Request) -> Response: 

293 """ 

294 For the specified ScienceReviewPanel, set isr.comment_for_pi and isr.individual_score unless they are AutoConflicted 

295 and update the state to SAved 

296 URL: testdata/set_isr_comments_scores 

297 

298 :param request: PUT request 

299 :return: 200 response (Success) Response with a list of JSON-formatted ISRs 

300 or 400 response (HTTPBadRequest) if expected srp_id is not given, or reference solicitation is not test/demo 

301 or 404 response (HTTPNotFound) if no scienceReviewPanel exists with the given srp_id or no solicitation 

302 exists for the srp specified 

303 """ 

304 srp = request.lookup(request.matchdict["srp_id"], ScienceReviewPanel) 

305 solicitation = request.lookup(srp.solicitation_id, Solicitation) 

306 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

307 context.set_isr_types(srp) 

308 context.set_isr_comments_and_scores(srp) 

309 isrs = context.get_isrs(srp) 

310 

311 return Response(json_body=[isr.__json__() for isr in isrs], status_code=HTTPStatus.OK) 

312 

313 

314@view_config( 

315 route_name="finalize_test_isrs", 

316 renderer="json", 

317 permission="finalize_test_isrs", 

318) 

319def finalize_test_isrs(request: Request) -> Response: 

320 """ 

321 For the specified ScienceReviewPanel, set finalize all ISRs as the reviewer for those ISRs (rather than as TTA 

322 member since that would normally close that ISR 

323 URL: testdata/finalize_test_isrs 

324 

325 :param request: PUT request 

326 :return: 200 response (Success) Response with a list of JSON-formatted ISRs 

327 or 400 response (HTTPBadRequest) if expected srp_id is not given, or reference solicitation is not test/demo 

328 or 404 response (HTTPNotFound) if no scienceReviewPanel exists with the given srp_id or no solicitation 

329 exists for the srp specified 

330 """ 

331 srp = request.lookup(request.matchdict["srp_id"], ScienceReviewPanel) 

332 solicitation = request.lookup(srp.solicitation_id, Solicitation) 

333 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

334 

335 context.finalize_test_isrs(srp) 

336 

337 isrs = context.get_isrs(srp) 

338 

339 return Response(json_body=[isr.__json__() for isr in isrs], status_code=HTTPStatus.OK) 

340 

341 

342@view_config( 

343 route_name="launch_consensus", 

344 renderer="json", 

345 permission="launch_consensus", 

346) 

347def launch_consensus(request: Request) -> Response: 

348 """ 

349 For the specified ScienceReviewPanel, launch the consensus phase 

350 URL: testdata/launch_consensus 

351 

352 :param request: PUT request 

353 :return: 200 response (Success) Response with a list of JSON-formatted PPRs 

354 Note: if consensus has already been initiated for this SRP, the existing PPRs will be returned. Also, no error 

355 will be raised. 

356 or 400 response (HTTPBadRequest) if expected srp_id is not given, or reference solicitation is not test/demo 

357 or 404 response (HTTPNotFound) if no scienceReviewPanel exists with the given srp_id 

358 or no solicitation exists for the srp specified 

359 """ 

360 srp = request.lookup(request.matchdict["srp_id"], ScienceReviewPanel) 

361 solicitation = request.lookup(srp.solicitation_id, Solicitation) 

362 try: 

363 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

364 pprs = context.launch_consensus(srp) 

365 except ValueError as e: 

366 raise HTTPBadRequest(body=str(e)) 

367 

368 return Response(json_body=[ppr.__json__() for ppr in pprs], status_code=HTTPStatus.OK) 

369 

370 

371@view_config( 

372 route_name="set_ppr_comments", 

373 renderer="json", 

374 permission="set_ppr_comments", 

375) 

376def set_ppr_comments(request: Request) -> Response: 

377 """ 

378 For the specified ScienceReviewPanel, update each PPR on a SRP with a concatenated list of ISR comments from each 

379 proposal 

380 URL: testdata/def set_ppr_comments(request: Request) -> Response: 

381 

382 :param request: PUT request 

383 :return: 200 response (Success) Response with a list of JSON-formatted ISRs 

384 or 400 response (HTTPBadRequest) if expected srp_id is not given, or reference solicitation is not test/demo 

385 or 404 response (HTTPNotFound) if no scienceReviewPanel exists with the given srp_id or no solicitation 

386 exists for the srp specified 

387 """ 

388 srp = request.lookup(request.matchdict["srp_id"], ScienceReviewPanel) 

389 solicitation = request.lookup(srp.solicitation_id, Solicitation) 

390 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

391 context.set_ppr_comments(srp) 

392 pprs = context.get_pprs(srp) 

393 

394 return Response(json_body=[ppr.__json__() for ppr in pprs], status_code=HTTPStatus.OK) 

395 

396 

397@view_config( 

398 route_name="complete_ppr_comments", 

399 renderer="json", 

400 permission="complete_ppr_comments", 

401) 

402def complete_ppr_comments(request: Request) -> Response: 

403 """ 

404 For the specified ScienceReviewPanel, update the state of PPRs with the SRP 

405 URL: testdata/complete_ppr_comments 

406 

407 :param request: PUT request 

408 :return: 200 response (Success) Response with a list of JSON-formatted ISRs 

409 or 400 response (HTTPBadRequest) if expected srp_id is not given, 

410 or reference solicitation is not test/demo 

411 or 404 response (HTTPNotFound) if no scienceReviewPanel exists with the given srp_id or no solicitation 

412 exists for the srp specified 

413 """ 

414 srp = request.lookup(request.matchdict["srp_id"], ScienceReviewPanel) 

415 solicitation = request.lookup(srp.solicitation_id, Solicitation) 

416 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

417 

418 # Confirm that all PPRs are in a non-Blank state, if one is found to be Blank error 

419 pprs = context.get_pprs(srp) 

420 

421 for ppr in pprs: 

422 if ppr.review_state == "Blank": 

423 raise HTTPBadRequest(body="One or more Consensus Reviews found to be in Blank review state") 

424 

425 context.set_complete_pprs(srp) 

426 pprs = context.get_pprs(srp) 

427 

428 return Response(json_body=[ppr.__json__() for ppr in pprs], status_code=HTTPStatus.OK) 

429 

430 

431@view_config( 

432 route_name="finalize_test_pprs", 

433 renderer="json", 

434 permission="finalize_test_pprs", 

435) 

436def finalize_test_pprs(request: Request) -> Response: 

437 """ 

438 For the specified ScienceReviewPanel, finalize the PPRs on the SRP 

439 URL: testdata/finalize_test_pprs 

440 

441 :param request: PUT request 

442 :return: 200 response (Success) Response with a list of JSON-formatted ISRs 

443 or 400 response (HTTPBadRequest) if expected srp_id is not given, 

444 or reference solicitation is not test/demo 

445 or 404 response (HTTPNotFound) if no scienceReviewPanel exists with the given srp_id or no solicitation 

446 exists for the srp specified 

447 """ 

448 srp = request.lookup(request.matchdict["srp_id"], ScienceReviewPanel) 

449 solicitation = request.lookup(srp.solicitation_id, Solicitation) 

450 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

451 

452 # Confirm that all PPRs are in a non-Blank state, if one is found to be Blank error 

453 pprs = context.get_pprs(srp) 

454 

455 for ppr in pprs: 

456 if ppr.review_state not in ["Completed", "Finalized"]: 

457 raise HTTPBadRequest( 

458 body="One or more Consensus Reviews found to be in a non-Completed or Finalized " "review state" 

459 ) 

460 

461 try: 

462 pprs = context.finalize_pprs(srp) 

463 except ValueError as e: 

464 raise HTTPBadRequest(body=str(e)) 

465 

466 return Response(json_body=[ppr.__json__() for ppr in pprs], status_code=HTTPStatus.OK) 

467 

468 

469@view_config( 

470 route_name="run_all_ppr_steps", 

471 renderer="json", 

472 permission="run_all_ppr_steps", 

473) 

474def run_all_ppr_steps(request: Request) -> Response: 

475 """ 

476 For the specified solicitation, run all the steps of the Panel Proposal Review process 

477 URL: testdata/run_all_ppr_process_steps/{solicitation_id} 

478 

479 :param request: POST request 

480 :return: 200 response (Success) Response with a dictionary containing SRPs 

481 or 400 response (HTTPBadRequest) if expected solicitation_id is not given, 

482 or reference solicitation is not test/demo 

483 or associated solicitation is not closed 

484 or attempt is made to run all steps against an OSR solicitation (not currently implemented in any form) 

485 or 404 response (HTTPNotFound) if no scienceReviewPanel exists with the given srp_id or no solicitation 

486 exists for the srp specified 

487 """ 

488 solicitation = request.lookup(request.matchdict["solicitation_id"], Solicitation) 

489 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

490 

491 if solicitation.proposal_process.proposal_process_name == "Panel Proposal Review": 

492 try: 

493 context.run_all_ppr_process_steps() 

494 except ValueError as e: 

495 raise HTTPBadRequest(body=str(e)) 

496 elif solicitation.proposal_process.proposal_process_name == "Observatory Site Review": 

497 try: 

498 context.run_all_osr_process_steps() 

499 except ValueError as e: 

500 raise HTTPBadRequest(body=str(e)) 

501 

502 srps = context.get_srps() 

503 

504 return Response(json_body=[srp.__json__() for srp in srps], status_code=HTTPStatus.CREATED) 

505 

506 

507@view_config( 

508 route_name="set_osr_comments", 

509 renderer="json", 

510 permission="set_osr_comments", 

511) 

512def set_osr_comments(request: Request) -> Response: 

513 """ 

514 For any Observatory Site Review proposals submitted and are in a Blank state, set the comments 

515 URL: testdata/set_osr_comments/{solicitation_id} 

516 

517 :param request: PUT request 

518 :return: 200 response (Success) Response with a list of JSON-formatted OSR proposal reviews 

519 or 400 response (HTTPBadRequest) if expected solicitation_id is not given, 

520 or reference solicitation is not test/demo 

521 or 404 response (HTTPNotFound) if the solicitation specified does not exist 

522 """ 

523 

524 solicitation = request.lookup(request.matchdict["solicitation_id"], Solicitation) 

525 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

526 

527 osr_prop_reviews = context.set_osr_comments() 

528 

529 # Currently, if none of the OSR proposal reviews are in a Blank state, success is returned 

530 

531 return Response(json_body=[osr.__json__() for osr in osr_prop_reviews], status_code=HTTPStatus.OK) 

532 

533 

534@view_config( 

535 route_name="set_osr_scientific_merit_metrics", 

536 renderer="json", 

537 permission="set_osr_scientific_merit_metrics", 

538) 

539def set_osr_scientific_merit_metrics(request: Request) -> Response: 

540 """ 

541 For any Observatory Site Review proposals submitted and are in a Saved state, set the scientific merit metrics 

542 URL: testdata/set_osr_scientific_merit_metrics/{solicitation_id} 

543 

544 :param request: PUT request 

545 :return: 200 response (Success) Response with a list of JSON-formatted OSR proposal reviews 

546 or 400 response (HTTPBadRequest) if expected solicitation_id is not given, 

547 or reference solicitation is not test/demo 

548 or 404 response (HTTPNotFound) if the solicitation specified does not exist 

549 """ 

550 

551 solicitation = request.lookup(request.matchdict["solicitation_id"], Solicitation) 

552 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

553 

554 osr_prop_reviews = context.set_osr_scientific_merit_metrics() 

555 

556 # Currently, if none of the OSR proposal reviews are in a Saved state, success is returned 

557 

558 return Response(json_body=[osr.__json__() for osr in osr_prop_reviews], status_code=HTTPStatus.OK) 

559 

560 

561@view_config( 

562 route_name="finalize_osr_reviews", 

563 renderer="json", 

564 permission="finalize_osr_reviews", 

565) 

566def finalize_osr_reviews(request: Request) -> Response: 

567 """ 

568 For any Observatory Site Review proposals submitted and are in a Saved state, and the scientific merit metric have been set, finalize the reviews 

569 URL: testdata/finalize_osr_reviews/{solicitation_id} 

570 

571 :param request: PUT request 

572 :return: 200 response (Success) Response with a list of JSON-formatted OSR proposal reviews 

573 or 400 response (HTTPBadRequest) if expected solicitation_id is not given, 

574 or reference solicitation is not test/demo 

575 or 404 response (HTTPNotFound) if the solicitation specified does not exist 

576 """ 

577 

578 solicitation = request.lookup(request.matchdict["solicitation_id"], Solicitation) 

579 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

580 

581 osr_prop_reviews = context.finalize_osr_reviews() 

582 

583 # Currently, if none of the OSR proposal reviews are in a Saved state, and the metric set to 0 or 1, success is returned 

584 

585 return Response(json_body=[osr.__json__() for osr in osr_prop_reviews], status_code=HTTPStatus.OK) 

586 

587 

588@view_config( 

589 route_name="run_all_osr_process_steps", 

590 renderer="json", 

591 permission="run_all_osr_process_steps", 

592) 

593def run_all_osr_process_steps(request: Request) -> Response: 

594 """ 

595 For any Observatory Site Review proposals submitted and are in a Saved state, and the scientific merit metric have been set, finalize the reviews 

596 URL: testdata/run_all_osr_process_steps/{solicitation_id} 

597 

598 :param request: PUT request 

599 :return: 200 response (Success) Response with a list of JSON-formatted OSR proposal reviews 

600 or 400 response (HTTPBadRequest) if expected solicitation_id is not given, 

601 or reference solicitation is not test/demo 

602 or 404 response (HTTPNotFound) if the solicitation specified does not exist 

603 """ 

604 

605 solicitation = request.lookup(request.matchdict["solicitation_id"], Solicitation) 

606 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

607 

608 context.run_all_osr_process_steps() 

609 

610 # Currently, success is returned along with all OSRs associated with the solicitation 

611 osr_prop_reviews = request.repo.osr_proposal_review_repo.list_by_solicitation_id(solicitation.solicitation_id) 

612 

613 return Response(json_body=[osr.__json__() for osr in osr_prop_reviews], status_code=HTTPStatus.OK) 

614 

615 

616@view_config( 

617 route_name="configure_tac_members", 

618 renderer="json", 

619 permission="allocate_publish", 

620) 

621def configure_tac_members(request: Request) -> Response: 

622 """ 

623 For the specified solicitation, configure TAC members 

624 URL: testdata/configure_tac_members/{{solicitation_id}} 

625 

626 :param request: POST request 

627 :return: 200 response (Success) Response with a dictionary containing SRPs 

628 or 400 response (HTTPBadRequest) if expected solicitation_id is not given, 

629 or publication destination is not valid 

630 or 404 response (HTTPNotFound) if no solicitation exists 

631 """ 

632 solicitation = request.lookup(request.matchdict["solicitation_id"], Solicitation) 

633 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

634 

635 try: 

636 context.configure_tac_members() 

637 except ValueError as e: 

638 raise HTTPBadRequest(body=str(e)) 

639 

640 tms = context.get_tac_members() 

641 

642 return Response(json_body=[tm.__json__() for tm in tms], status_code=HTTPStatus.CREATED) 

643 

644 

645@view_config( 

646 route_name="allocate_publish", 

647 renderer="json", 

648 permission="allocate_publish", 

649) 

650@view_config( 

651 route_name="allocate_publish_with_pdg", 

652 renderer="json", 

653 permission="allocate_publish", 

654) 

655def allocate_publish(request: Request) -> Response: 

656 """ 

657 For the specified solicitation, run all the steps of allocation and publish to the desired destination 

658 URL: testdata/allocate_publish/{{solicitation_id}}/{{publication_destination}} or 

659 testdata/allocate_publish/{{solicitation_id}}/{{publication_destination}}/{{proposal_disposition_group_id}} 

660 

661 :param request: POST request 

662 :return: 200 response (Success) Response with a dictionary containing SRPs 

663 or 400 response (HTTPBadRequest) if expected solicitation_id is not given, 

664 or publication destination is not valid 

665 or 404 response (HTTPNotFound) if no solicitation exists or there is no proposalDispositionGroup associated with 

666 the optionally provided proposal_disposition_id 

667 """ 

668 solicitation = request.lookup(request.matchdict["solicitation_id"], Solicitation) 

669 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

670 

671 pdg = None 

672 proposal_disposition_group_id = request.matchdict.get("proposal_disposition_group_id") 

673 if proposal_disposition_group_id: 

674 pdg = request.lookup(proposal_disposition_group_id, ProposalDispositionGroup) 

675 

676 # obtain the destination from the URL 

677 dname: str = request.matchdict["publication_destination"] 

678 

679 # attempt to find it 

680 try: 

681 destination = PublicationDestination(dname.upper()) 

682 except ValueError: 

683 raise HTTPBadRequest(body=f"Unable to publish allocation version to unknown destination {dname}") 

684 

685 try: 

686 context.generate_and_publish(destination, pdg) 

687 except ValueError as e: 

688 raise HTTPBadRequest(body=str(e)) 

689 

690 pdgs = [pdg] if proposal_disposition_group_id else context.get_proposal_disposition_groups() 

691 

692 return Response(json_body=[pdg.__json__() for pdg in pdgs], status_code=HTTPStatus.CREATED) 

693 

694 

695@view_config( 

696 route_name="generate_proposal_disposition_comments", 

697 renderer="json", 

698 permission="allocate_publish", 

699) 

700@view_config( 

701 route_name="generate_proposal_disposition_comments_with_pdg", 

702 renderer="json", 

703 permission="allocate_publish", 

704) 

705def generate_proposal_disposition_comments(request: Request) -> Response: 

706 """ 

707 For the specified solicitation, generate comments for each proposal disposition 

708 URL: testdata/generate_proposal_disposition_comments/{{solicitation_id}} or 

709 testdata/generate_proposal_disposition_comments/{{solicitation_id}}/{{proposal_disposition_group_id}} 

710 

711 :param request: PUT request 

712 :return: 200 response (Success) Response with a dictionary containing PDGs 

713 or 400 response (HTTPBadRequest) if expected solicitation_id is not given 

714 or 404 response (HTTPNotFound) if no solicitation exists or there is no proposalDispositionGroup associated with 

715 the optionally provided proposal_disposition_id 

716 """ 

717 solicitation = request.lookup(request.matchdict["solicitation_id"], Solicitation) 

718 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

719 

720 pdg = None 

721 proposal_disposition_group_id = request.matchdict.get("proposal_disposition_group_id") 

722 if proposal_disposition_group_id: 

723 pdg = request.lookup(proposal_disposition_group_id, ProposalDispositionGroup) 

724 

725 try: 

726 context.generate_proposal_disposition_comments(pdg) 

727 except ValueError as e: 

728 raise HTTPBadRequest(body=str(e)) 

729 

730 pdgs = [pdg] if proposal_disposition_group_id else context.get_proposal_disposition_groups() 

731 

732 return Response(json_body=[pdg.__json__() for pdg in pdgs], status_code=HTTPStatus.CREATED) 

733 

734 

735@view_config( 

736 route_name="allocate_advance_to_end", 

737 renderer="json", 

738 permission="allocate_publish", 

739) 

740@view_config( 

741 route_name="allocate_advance_to_end_with_pdg", 

742 renderer="json", 

743 permission="allocate_publish", 

744) 

745def allocate_advance_to_end(request: Request) -> Response: 

746 """ 

747 For the specified solicitation, run all the steps of allocation and publish to the desired destination 

748 URL: testdata/allocate_advance_to_end/{{solicitation_id}} or 

749 testdata/allocate_advance_to_end/{{solicitation_id}}/{{proposal_disposition_group_id}} 

750 

751 :param request: PUT request where proposal_disposition_group_id parametr is optional 

752 :return: 200 response (Success) Response with a dictionary containing SRPs 

753 or 400 response (HTTPBadRequest) if expected solicitation_id is not given, 

754 or 404 response (HTTPNotFound) if no solicitation exists or there is no proposalDispositionGroup associated with 

755 the optionally provided proposal_disposition_id 

756 """ 

757 solicitation = request.lookup(request.matchdict["solicitation_id"], Solicitation) 

758 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

759 

760 pdg = None 

761 proposal_disposition_group_id = request.matchdict.get("proposal_disposition_group_id") 

762 if proposal_disposition_group_id: 

763 pdg = request.lookup(proposal_disposition_group_id, ProposalDispositionGroup) 

764 

765 try: 

766 context.advance_to_end_allocate_and_accept(pdg) 

767 except ValueError as e: 

768 raise HTTPBadRequest(body=str(e)) 

769 

770 pdgs = context.get_proposal_disposition_groups() 

771 

772 return Response(json_body=[pdg.__json__() for pdg in pdgs], status_code=HTTPStatus.CREATED) 

773 

774 

775@view_config( 

776 route_name="closeout_advance_to_end", 

777 renderer="json", 

778 permission="update_disposition_letter", 

779) 

780@view_config( 

781 route_name="closeout_advance_to_end_with_pdg", 

782 renderer="json", 

783 permission="update_disposition_letter", 

784) 

785def closeout_advance_to_end(request: Request) -> Response: 

786 """ 

787 For the specified solicitation, generate and send disposition letters 

788 URL: testdata/closeout_advance_to_end/{{solicitation_id}} or 

789 testdata/closeout_advance_to_end/{{solicitation_id}}/{{proposal_disposition_group_id}} 

790 

791 :param request: PUT request where proposal_disposition_group_id parametr is optional 

792 :return: 201 response (Success) Response with list of proposal disposition groups 

793 or 400 response (HTTPBadRequest) if expected solicitation_id is not given, 

794 or 404 response (HTTPNotFound) if no solicitation exists or there is no proposalDispositionGroup associated with 

795 the optionally provided proposal_disposition_id 

796 """ 

797 solicitation = request.lookup(request.matchdict["solicitation_id"], Solicitation) 

798 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

799 

800 pdg = None 

801 proposal_disposition_group_id = request.matchdict.get("proposal_disposition_group_id") 

802 if proposal_disposition_group_id: 

803 pdg = request.lookup(proposal_disposition_group_id, ProposalDispositionGroup) 

804 

805 try: 

806 context.advance_to_end_closeout(pdg) 

807 except ValueError as e: 

808 raise HTTPBadRequest(body=str(e)) 

809 

810 pdgs = context.get_proposal_disposition_groups() 

811 

812 return Response(json_body=[pdg.__json__() for pdg in pdgs], status_code=HTTPStatus.CREATED) 

813 

814 

815@view_config( 

816 route_name="advance_to_project_creation", 

817 renderer="json", 

818 permission="export_prototype_project", 

819) 

820@view_config( 

821 route_name="advance_to_project_creation_with_pdg", 

822 renderer="json", 

823 permission="export_prototype_project", 

824) 

825def advance_to_project_creation(request: Request) -> Response: 

826 """ 

827 For the specified solicitation, export projects 

828 URL: testdata/advance_to_project_creation/{{solicitation_id}} or 

829 testdata/advance_to_project_creation/{{solicitation_id}}/{{proposal_disposition_group_id}} 

830 

831 :param request: PUT request where proposal_disposition_group_id parametr is optional 

832 :return: 201 response (HTTPCreated) Response with a list of proposal disposition groups 

833 or 400 response (HTTPBadRequest) if expected solicitation_id is not given, 

834 or 404 response (HTTPNotFound) if no solicitation exists or there is no proposalDispositionGroup associated with 

835 the optionally provided proposal_disposition_id 

836 """ 

837 solicitation = request.lookup(request.matchdict["solicitation_id"], Solicitation) 

838 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

839 

840 pdg = None 

841 proposal_disposition_group_id = request.matchdict.get("proposal_disposition_group_id") 

842 if proposal_disposition_group_id: 

843 pdg = request.lookup(proposal_disposition_group_id, ProposalDispositionGroup) 

844 

845 try: 

846 context.advance_to_end_project_creation(pdg) 

847 except ValueError as e: 

848 raise HTTPBadRequest(body=str(e)) 

849 

850 pdgs = context.get_proposal_disposition_groups() 

851 

852 return Response(json_body=[pdg.__json__() for pdg in pdgs], status_code=HTTPStatus.CREATED) 

853 

854 

855@view_config( 

856 route_name="cleanup", 

857 renderer="json", 

858 permission="cleanup", 

859) 

860def cleanup(request: Request) -> Response: 

861 """ 

862 Deletes this solicitation and its children 

863 URL: testdata/{solicitation_id} 

864 

865 :param request: DELETE request 

866 :return: 200 response (Success) Response with json of the deleted solicitation 

867 or 404 response (HTTPNotFound) if no solicitation is found 

868 """ 

869 solicitation = request.lookup(request.matchdict["solicitation_id"], Solicitation) 

870 context = get_context(request.repo.session, solicitation_id=solicitation.solicitation_id) 

871 context.cleanup() 

872 return Response(json_body=solicitation.__json__(), status_code=HTTPStatus.OK)