4545 "Edvise Schema (ES) (set edvise_id or is_edvise), or Legacy "
4646 "(set legacy_id or is_legacy)."
4747)
48+ _MUTUALLY_EXCLUSIVE_SCHOOL_TYPE_DETAIL = (
49+ "An institution cannot be more than one of PDP, Edvise Schema (ES), or Legacy. "
50+ "Please choose one schema type."
51+ )
4852
4953
5054class InstitutionCreationRequest (BaseModel ):
@@ -162,6 +166,154 @@ def _compute_edvise_legacy_ids_for_create(
162166 return (edvise_id , legacy_id )
163167
164168
169+ def _raise_if_existing_row_invalid_for_duplicate_post (existing : InstTable ) -> None :
170+ """Reject idempotent POST when the stored row violates school-type invariants."""
171+ ep , ee , el = existing .pdp_id , existing .edvise_id , existing .legacy_id
172+ if not has_at_most_one_school_type (ep , ee , el ):
173+ raise HTTPException (
174+ status_code = status .HTTP_400_BAD_REQUEST ,
175+ detail = _MUTUALLY_EXCLUSIVE_SCHOOL_TYPE_DETAIL ,
176+ )
177+ if sum (bool (x ) for x in (ep , ee , el )) != 1 :
178+ raise HTTPException (
179+ status_code = status .HTTP_400_BAD_REQUEST ,
180+ detail = (
181+ "An institution with this name and state already exists but does not "
182+ "have a valid school type configured. "
183+ + _EXACTLY_ONE_SCHOOL_TYPE_DETAIL
184+ ),
185+ )
186+
187+
188+ def _raise_if_institution_name_patch_disallowed (
189+ update_data : dict , existing_name : str
190+ ) -> None :
191+ if "name" in update_data and update_data ["name" ] != existing_name :
192+ raise HTTPException (
193+ status_code = status .HTTP_400_BAD_REQUEST ,
194+ detail = "Institution names cannot be changed." ,
195+ )
196+
197+
198+ def _normalize_patch_school_id_strings (update_data : dict ) -> None :
199+ for key in ("pdp_id" , "edvise_id" , "legacy_id" ):
200+ if key in update_data :
201+ update_data [key ] = (update_data [key ] or "" ).strip () or None
202+
203+
204+ def _resolve_merged_school_type_triple_for_patch (
205+ existing_inst : InstTable ,
206+ update_data : dict ,
207+ sess : Session ,
208+ ) -> Tuple [Optional [str ], Optional [str ], Optional [str ], bool ]:
209+ """Merge PATCH school-type fields, auto-assign ids, enforce exactly one type."""
210+ old_type_triple = (
211+ existing_inst .pdp_id ,
212+ existing_inst .edvise_id ,
213+ existing_inst .legacy_id ,
214+ )
215+ _raise_if_institution_name_patch_disallowed (update_data , existing_inst .name )
216+ _normalize_patch_school_id_strings (update_data )
217+ final_pdp_id = (
218+ update_data ["pdp_id" ] if "pdp_id" in update_data else existing_inst .pdp_id
219+ )
220+ final_edvise_id = (
221+ update_data ["edvise_id" ]
222+ if "edvise_id" in update_data
223+ else existing_inst .edvise_id
224+ )
225+ final_legacy_id = (
226+ update_data ["legacy_id" ]
227+ if "legacy_id" in update_data
228+ else existing_inst .legacy_id
229+ )
230+ if _patch_indicates_more_than_one_school_type (
231+ update_data , final_pdp_id , final_edvise_id , final_legacy_id
232+ ):
233+ raise HTTPException (
234+ status_code = status .HTTP_400_BAD_REQUEST ,
235+ detail = _MUTUALLY_EXCLUSIVE_SCHOOL_TYPE_DETAIL ,
236+ )
237+ final_edvise_id , final_legacy_id = _compute_edvise_legacy_ids_for_patch (
238+ sess , update_data , final_edvise_id , final_legacy_id
239+ )
240+ if not has_at_most_one_school_type (final_pdp_id , final_edvise_id , final_legacy_id ):
241+ raise HTTPException (
242+ status_code = status .HTTP_400_BAD_REQUEST ,
243+ detail = _MUTUALLY_EXCLUSIVE_SCHOOL_TYPE_DETAIL ,
244+ )
245+ if sum (bool (x ) for x in (final_pdp_id , final_edvise_id , final_legacy_id )) != 1 :
246+ raise HTTPException (
247+ status_code = status .HTTP_400_BAD_REQUEST ,
248+ detail = _EXACTLY_ONE_SCHOOL_TYPE_DETAIL ,
249+ )
250+ school_type_changed = old_type_triple != (
251+ final_pdp_id ,
252+ final_edvise_id ,
253+ final_legacy_id ,
254+ )
255+ return final_pdp_id , final_edvise_id , final_legacy_id , school_type_changed
256+
257+
258+ def _require_single_institution_row_by_uuid (sess : Session , inst_id : str ) -> InstTable :
259+ """Load exactly one InstTable row by UUID or raise HTTP 400."""
260+ query_result = (
261+ sess .execute (
262+ select (InstTable ).where (InstTable .id == str_to_uuid (inst_id ))
263+ ).all ()
264+ )
265+ if not query_result or len (query_result ) != 1 :
266+ raise HTTPException (
267+ status_code = status .HTTP_400_BAD_REQUEST ,
268+ detail = "Unexpected number of institutions found with this id. Expected 1 got "
269+ + str (len (query_result )),
270+ )
271+ return query_result [0 ][0 ]
272+
273+
274+ def _persist_institution_patch_row_fields (
275+ existing_inst : InstTable ,
276+ update_data : dict ,
277+ final_pdp_id : Optional [str ],
278+ final_edvise_id : Optional [str ],
279+ final_legacy_id : Optional [str ],
280+ ) -> None :
281+ """Apply non-schema PATCH fields and the resolved school-type triple to the ORM row."""
282+ if "state" in update_data :
283+ existing_inst .state = update_data ["state" ]
284+ if "allowed_emails" in update_data :
285+ existing_inst .allowed_emails = update_data ["allowed_emails" ]
286+ if "retention_days" in update_data :
287+ existing_inst .retention_days = update_data ["retention_days" ]
288+ existing_inst .pdp_id = final_pdp_id
289+ existing_inst .edvise_id = final_edvise_id
290+ existing_inst .legacy_id = final_legacy_id
291+
292+
293+ def _apply_institution_schema_updates_from_patch (
294+ existing_inst : InstTable ,
295+ update_data : dict ,
296+ school_type_changed : bool ,
297+ final_pdp_id : Optional [str ],
298+ final_edvise_id : Optional [str ],
299+ final_legacy_id : Optional [str ],
300+ ) -> None :
301+ if school_type_changed :
302+ extra_allowed = (
303+ update_data ["allowed_schemas" ]
304+ if "allowed_schemas" in update_data
305+ else None
306+ )
307+ existing_inst .schemas = _build_requested_schemas (
308+ extra_allowed ,
309+ final_pdp_id ,
310+ final_edvise_id ,
311+ final_legacy_id ,
312+ )
313+ elif "allowed_schemas" in update_data :
314+ existing_inst .schemas = update_data ["allowed_schemas" ]
315+
316+
165317def _patch_indicates_more_than_one_school_type (
166318 update_data : dict ,
167319 pdp_id : Optional [str ],
@@ -275,7 +427,7 @@ def _validate_and_prepare_create_institution(
275427 if _request_has_more_than_one_school_type (req , pdp_id , edvise_id , legacy_id ):
276428 raise HTTPException (
277429 status_code = status .HTTP_400_BAD_REQUEST ,
278- detail = "An institution cannot be more than one of PDP, Edvise Schema (ES), or Legacy. Please choose one schema type." ,
430+ detail = _MUTUALLY_EXCLUSIVE_SCHOOL_TYPE_DETAIL ,
279431 )
280432 local_session .set (sql_session )
281433 sess = local_session .get ()
@@ -285,7 +437,7 @@ def _validate_and_prepare_create_institution(
285437 if not has_at_most_one_school_type (pdp_id , edvise_id , legacy_id ):
286438 raise HTTPException (
287439 status_code = status .HTTP_400_BAD_REQUEST ,
288- detail = "An institution cannot be more than one of PDP, Edvise Schema (ES), or Legacy. Please choose one schema type." ,
440+ detail = _MUTUALLY_EXCLUSIVE_SCHOOL_TYPE_DETAIL ,
289441 )
290442 school_type_count = sum (bool (x ) for x in (pdp_id , edvise_id , legacy_id ))
291443 if school_type_count != 1 :
@@ -405,21 +557,7 @@ def create_institution(
405557 )
406558 else :
407559 existing = query_result [0 ][0 ]
408- ep , ee , el = existing .pdp_id , existing .edvise_id , existing .legacy_id
409- if not has_at_most_one_school_type (ep , ee , el ):
410- raise HTTPException (
411- status_code = status .HTTP_400_BAD_REQUEST ,
412- detail = "An institution cannot be more than one of PDP, Edvise Schema (ES), or Legacy. Please choose one schema type." ,
413- )
414- if sum (bool (x ) for x in (ep , ee , el )) != 1 :
415- raise HTTPException (
416- status_code = status .HTTP_400_BAD_REQUEST ,
417- detail = (
418- "An institution with this name and state already exists but does not "
419- "have a valid school type configured. "
420- + _EXACTLY_ONE_SCHOOL_TYPE_DETAIL
421- ),
422- )
560+ _raise_if_existing_row_invalid_for_duplicate_post (existing )
423561 row = existing
424562 if len (query_result ) > 1 :
425563 raise HTTPException (
@@ -450,131 +588,36 @@ def update_inst(
450588 status_code = status .HTTP_401_UNAUTHORIZED ,
451589 detail = "Not authorized to modify an institution." ,
452590 )
453-
454591 update_data = request .model_dump (exclude_unset = True )
455592 local_session .set (sql_session )
456- # Check that the batch exists.
457- query_result = (
458- local_session .get ()
459- .execute (
460- select (InstTable ).where (
461- InstTable .id == str_to_uuid (inst_id ),
462- )
463- )
464- .all ()
465- )
466- if not query_result or len (query_result ) != 1 :
467- raise HTTPException (
468- status_code = status .HTTP_400_BAD_REQUEST ,
469- detail = "Unexpected number of institutions found with this id. Expected 1 got "
470- + str (len (query_result )),
471- )
472- existing_inst = query_result [0 ][0 ]
473- old_type_triple = (
474- existing_inst .pdp_id ,
475- existing_inst .edvise_id ,
476- existing_inst .legacy_id ,
477- )
478- if "name" in update_data :
479- if update_data ["name" ] != existing_inst .name :
480- raise HTTPException (
481- status_code = status .HTTP_400_BAD_REQUEST ,
482- detail = "Institution names cannot be changed." ,
483- )
484- # Normalize empty strings to None for consistency
485- # Strip whitespace and convert empty strings to None
486- if "pdp_id" in update_data :
487- update_data ["pdp_id" ] = (update_data ["pdp_id" ] or "" ).strip () or None
488- if "edvise_id" in update_data :
489- update_data ["edvise_id" ] = (update_data ["edvise_id" ] or "" ).strip () or None
490- if "legacy_id" in update_data :
491- update_data ["legacy_id" ] = (update_data ["legacy_id" ] or "" ).strip () or None
492- # Merge patch into existing row (before is_* auto-assign, same as POST ordering)
493- final_pdp_id = (
494- update_data .get ("pdp_id" ) if "pdp_id" in update_data else existing_inst .pdp_id
495- )
496- final_edvise_id = (
497- update_data .get ("edvise_id" )
498- if "edvise_id" in update_data
499- else existing_inst .edvise_id
500- )
501- final_legacy_id = (
502- update_data .get ("legacy_id" )
503- if "legacy_id" in update_data
504- else existing_inst .legacy_id
505- )
506- if _patch_indicates_more_than_one_school_type (
507- update_data , final_pdp_id , final_edvise_id , final_legacy_id
508- ):
509- raise HTTPException (
510- status_code = status .HTTP_400_BAD_REQUEST ,
511- detail = "An institution cannot be more than one of PDP, Edvise Schema (ES), or Legacy. Please choose one schema type." ,
512- )
513593 sess = local_session .get ()
514- final_edvise_id , final_legacy_id = _compute_edvise_legacy_ids_for_patch (
515- sess , update_data , final_edvise_id , final_legacy_id
594+ existing_inst = _require_single_institution_row_by_uuid (sess , inst_id )
595+ (
596+ final_pdp_id ,
597+ final_edvise_id ,
598+ final_legacy_id ,
599+ school_type_changed ,
600+ ) = _resolve_merged_school_type_triple_for_patch (
601+ existing_inst , update_data , sess
516602 )
517- if not has_at_most_one_school_type (final_pdp_id , final_edvise_id , final_legacy_id ):
518- raise HTTPException (
519- status_code = status .HTTP_400_BAD_REQUEST ,
520- detail = "An institution cannot be more than one of PDP, Edvise Schema (ES), or Legacy. Please choose one schema type." ,
521- )
522- school_type_count = sum (
523- bool (x ) for x in (final_pdp_id , final_edvise_id , final_legacy_id )
603+ _persist_institution_patch_row_fields (
604+ existing_inst ,
605+ update_data ,
606+ final_pdp_id ,
607+ final_edvise_id ,
608+ final_legacy_id ,
524609 )
525- if school_type_count != 1 :
526- raise HTTPException (
527- status_code = status .HTTP_400_BAD_REQUEST ,
528- detail = _EXACTLY_ONE_SCHOOL_TYPE_DETAIL ,
529- )
530-
531- new_type_triple = (final_pdp_id , final_edvise_id , final_legacy_id )
532- school_type_changed = old_type_triple != new_type_triple
533-
534- if "state" in update_data :
535- existing_inst .state = update_data ["state" ]
536- if "allowed_emails" in update_data :
537- existing_inst .allowed_emails = update_data ["allowed_emails" ]
538- if "retention_days" in update_data :
539- existing_inst .retention_days = update_data ["retention_days" ]
540- # Persist merged type triple (covers partial PATCH and is_edvise/is_legacy auto-ids).
541- existing_inst .pdp_id = final_pdp_id
542- existing_inst .edvise_id = final_edvise_id
543- existing_inst .legacy_id = final_legacy_id
544-
545- # Recompute schemas only when the resolved school-type triple changes.
546- if school_type_changed :
547- extra_allowed = (
548- update_data ["allowed_schemas" ] if "allowed_schemas" in update_data else None
549- )
550- existing_inst .schemas = _build_requested_schemas (
551- extra_allowed ,
552- final_pdp_id ,
553- final_edvise_id ,
554- final_legacy_id ,
555- )
556- elif "allowed_schemas" in update_data :
557- existing_inst .schemas = update_data ["allowed_schemas" ]
558-
559- local_session .get ().commit ()
560- res = (
561- local_session .get ()
562- .execute (
563- select (InstTable ).where (
564- InstTable .id == str_to_uuid (inst_id ),
565- )
566- )
567- .all ()
610+ _apply_institution_schema_updates_from_patch (
611+ existing_inst ,
612+ update_data ,
613+ school_type_changed ,
614+ final_pdp_id ,
615+ final_edvise_id ,
616+ final_legacy_id ,
568617 )
569- return {
570- "inst_id" : uuid_to_str (res [0 ][0 ].id ),
571- "name" : res [0 ][0 ].name ,
572- "state" : res [0 ][0 ].state ,
573- "pdp_id" : res [0 ][0 ].pdp_id ,
574- "edvise_id" : res [0 ][0 ].edvise_id ,
575- "legacy_id" : res [0 ][0 ].legacy_id ,
576- "retention_days" : res [0 ][0 ].retention_days ,
577- }
618+ sess .commit ()
619+ refreshed = _require_single_institution_row_by_uuid (sess , inst_id )
620+ return _institution_row_to_response (refreshed )
578621
579622
580623@router .delete ("/institutions/{inst_id}" , response_model = None )
0 commit comments