From 17de0ee68cd831f31df48e5f200c7a9562fbb03a Mon Sep 17 00:00:00 2001 From: DIodide Date: Sat, 28 Mar 2026 00:24:42 -0400 Subject: [PATCH] Fix stale settled paths after hoagie data update The hoagie requirements data update (#514) changed category names in the YAML files, breaking the verifier's settled path matching for existing users. This adds a management command to clear stale settled paths and also: - Adds POR/SPA to AB_CONCENTRATIONS (new local YAML files) - Cleans up verifier imports (removes unused `requests`, fixes `pathlib` placement) - Management command also creates Major DB records for POR/SPA Run on production after deploy: docker compose -f docker-compose.prod.yml exec -T web python manage.py clear_settled_paths --apply Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scripts/university_info.py | 2 + .../scripts/verifier.py | 7 +- .../commands/clear_settled_paths.py | 98 +++++++++++++++++++ 3 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 tigerpath/management/commands/clear_settled_paths.py diff --git a/tigerpath/majors_and_certificates/scripts/university_info.py b/tigerpath/majors_and_certificates/scripts/university_info.py index 01ceaacf..09c61452 100644 --- a/tigerpath/majors_and_certificates/scripts/university_info.py +++ b/tigerpath/majors_and_certificates/scripts/university_info.py @@ -172,10 +172,12 @@ "PHI": "Philosophy", "PHY": "Physics", "POL": "Politics", + "POR": "Portuguese", "PSY": "Psychology", "REL": "Religion", "SLA": "Slavic Languages and Literatures", "SOC": "Sociology", + "SPA": "Spanish", "SPI": "Public and International Affairs", "SPO": "Spanish and Portuguese", } diff --git a/tigerpath/majors_and_certificates/scripts/verifier.py b/tigerpath/majors_and_certificates/scripts/verifier.py index 54950ab9..6acdb2fb 100644 --- a/tigerpath/majors_and_certificates/scripts/verifier.py +++ b/tigerpath/majors_and_certificates/scripts/verifier.py @@ -2,19 +2,16 @@ import collections import copy import os +import pathlib from functools import lru_cache -import requests import yaml from . import university_info -# Allow overriding the data repo base via env var for easy testing -# Must end with a trailing slash and point to a raw.githubusercontent.com base -import pathlib - _LOCAL_DATA_DIR = pathlib.Path(__file__).resolve().parent.parent.parent / "requirements_data" + @lru_cache(maxsize=256) def _load_yaml(path: str): """Load and parse YAML from the local requirements data directory.""" diff --git a/tigerpath/management/commands/clear_settled_paths.py b/tigerpath/management/commands/clear_settled_paths.py new file mode 100644 index 00000000..17232a48 --- /dev/null +++ b/tigerpath/management/commands/clear_settled_paths.py @@ -0,0 +1,98 @@ +""" +Clear stale 'settled' paths from all users' schedules and add new Major records. + +When the requirement YAML data is updated with new category names, existing users' +settled paths (stored in user_schedule JSON) no longer match the new requirement +tree structure. This command clears those stale paths and adds Major records for +newly available concentrations. + +The verifier's auto-settle feature will re-assign courses that can only satisfy +one requirement; users only need to manually re-settle ambiguous courses. + +Usage: + python manage.py clear_settled_paths # dry-run (default) + python manage.py clear_settled_paths --apply # actually write changes +""" + +from django.core.management.base import BaseCommand + +from tigerpath.models import Major, UserProfile + +NEW_MAJORS = [ + {"name": "Portuguese", "code": "POR", "degree": "AB"}, + {"name": "Spanish", "code": "SPA", "degree": "AB"}, +] + + +class Command(BaseCommand): + help = "Clear stale settled paths and add new Major records for updated requirement data" + + def add_arguments(self, parser): + parser.add_argument( + "--apply", + action="store_true", + default=False, + help="Actually write changes to the database (default is dry-run)", + ) + + def handle(self, *args, **options): + apply = options["apply"] + mode = "APPLIED" if apply else "DRY-RUN" + + # --- Step 1: Add new Major records --- + self.stdout.write(f"\n[{mode}] Step 1: Adding new Major records") + for m in NEW_MAJORS: + exists = Major.objects.filter(code=m["code"]).exists() + if exists: + self.stdout.write(f" {m['code']} already exists, skipping") + else: + self.stdout.write(f" {m['code']} ({m['name']}) - will create") + if apply: + Major.objects.create( + name=m["name"], + code=m["code"], + degree=m["degree"], + supported=True, + ) + + # --- Step 2: Clear settled paths --- + self.stdout.write(f"\n[{mode}] Step 2: Clearing settled paths from user schedules") + profiles = UserProfile.objects.exclude(user_schedule__isnull=True) + total = profiles.count() + affected = 0 + courses_cleared = 0 + + for profile in profiles.iterator(): + schedule = profile.user_schedule + if not schedule: + continue + + modified = False + for semester in schedule: + if not isinstance(semester, list): + continue + for course in semester: + if not isinstance(course, dict): + continue + settled = course.get("settled") + if settled and isinstance(settled, list) and len(settled) > 0: + course["settled"] = [] + modified = True + courses_cleared += 1 + + if modified: + affected += 1 + if apply: + profile.user_schedule = schedule + profile.save(update_fields=["user_schedule"]) + + self.stdout.write(f"\n[{mode}] Results:") + self.stdout.write(f" Scanned {total} profiles") + self.stdout.write(f" {affected} profiles had settled paths") + self.stdout.write(f" {courses_cleared} course-requirement bindings cleared") + if not apply: + self.stdout.write( + self.style.WARNING("\nNo changes written. Pass --apply to commit changes.") + ) + else: + self.stdout.write(self.style.SUCCESS("\nDone. Migration complete."))