Skip to content

Commit 83b8f61

Browse files
committed
refactor(institutions): extract PATCH helpers and DRY school-type errors
- Add shared mutual-exclusion detail constant for POST/PATCH paths - Extract duplicate-post row validation and PATCH merge/validate/persist helpers - Keep update_inst within single-responsibility helpers; reuse row response mapper Made-with: Cursor
1 parent 93ebced commit 83b8f61

1 file changed

Lines changed: 179 additions & 136 deletions

File tree

src/webapp/routers/institutions.py

Lines changed: 179 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@
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

5054
class 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+
165317
def _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

Comments
 (0)