diff --git a/courses/management/commands/sync_external_course_runs.py b/courses/management/commands/sync_external_course_runs.py index 4243d7da8..c0abfc002 100644 --- a/courses/management/commands/sync_external_course_runs.py +++ b/courses/management/commands/sync_external_course_runs.py @@ -3,8 +3,7 @@ from django.core.management.base import BaseCommand from courses.sync_external_courses.external_course_sync_api import ( - EMERITUS_PLATFORM_NAME, - GLOBAL_ALUMNI_PLATFORM_NAME, + VENDOR_KEYMAPS, EmeritusKeyMap, GlobalAlumniKeyMap, fetch_external_courses, @@ -39,13 +38,11 @@ def handle(self, *args, **options): # noqa: ARG002 return vendor_name = options["vendor_name"] - if vendor_name.lower() == EMERITUS_PLATFORM_NAME.lower(): - keymap = EmeritusKeyMap() - elif vendor_name.lower() == GLOBAL_ALUMNI_PLATFORM_NAME.lower(): - keymap = GlobalAlumniKeyMap() - else: + keymap = VENDOR_KEYMAPS.get(vendor_name.lower()) + if not keymap: self.stdout.write(self.style.ERROR(f"Unknown vendor name {vendor_name}.")) return + self.stdout.write(f"Starting course sync for {vendor_name}.") emeritus_course_runs = fetch_external_courses(keymap) stats = update_external_course_runs(emeritus_course_runs, keymap) diff --git a/courses/migrations/0041_platform_sync_daily.py b/courses/migrations/0041_platform_sync_daily.py new file mode 100644 index 000000000..000d5b2bf --- /dev/null +++ b/courses/migrations/0041_platform_sync_daily.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2024-12-04 07:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('courses', '0040_alter_courserun_courseware_id'), + ] + + operations = [ + migrations.AddField( + model_name='platform', + name='sync_daily', + field=models.BooleanField(default=False), + ), + ] diff --git a/courses/models.py b/courses/models.py index 9cbf6cae8..28c1e3889 100644 --- a/courses/models.py +++ b/courses/models.py @@ -214,6 +214,7 @@ class Platform(TimestampedModel, ValidateOnSaveMixin): """ name = models.CharField(max_length=255, unique=True) + sync_daily = models.BooleanField(default=False) def __str__(self): return self.name diff --git a/courses/sync_external_courses/external_course_sync_api.py b/courses/sync_external_courses/external_course_sync_api.py index 572ef3f92..8c5e09bf9 100644 --- a/courses/sync_external_courses/external_course_sync_api.py +++ b/courses/sync_external_courses/external_course_sync_api.py @@ -30,9 +30,6 @@ log = logging.getLogger(__name__) -EMERITUS_PLATFORM_NAME = "Emeritus" -GLOBAL_ALUMNI_PLATFORM_NAME = "Global Alumni" - class BaseKeyMap: """ Base class for course sync keys with common attributes. @@ -52,11 +49,11 @@ def __init__(self, platform_name, report_names): self.report_names = report_names @property - def COURSE_PAGE_SUBHEAD(self): + def course_page_subhead(self): return f"Delivered in collaboration with {self.platform_name}." @property - def LEARNING_OUTCOMES_PAGE_SUBHEAD(self): + def learning_outcomes_page_subhead(self): return ( f"MIT xPRO is collaborating with online education provider {self.platform_name} to " "deliver this online course. By clicking LEARN MORE, you will be taken to " @@ -70,7 +67,7 @@ class EmeritusKeyMap(BaseKeyMap): Emeritus course sync keys. """ def __init__(self): - super().__init__(platform_name=EMERITUS_PLATFORM_NAME, report_names=["Batch"]) + super().__init__(platform_name="Emeritus", report_names=["Batch"]) class GlobalAlumniKeyMap(BaseKeyMap): @@ -78,8 +75,12 @@ class GlobalAlumniKeyMap(BaseKeyMap): Global Alumni course sync keys. """ def __init__(self): - super().__init__(platform_name=GLOBAL_ALUMNI_PLATFORM_NAME, report_names=["GA - Batch"]) + super().__init__(platform_name="Global Alumni", report_names=["GA - Batch"]) +VENDOR_KEYMAPS = { + "emeritus": EmeritusKeyMap(), + "global alumni": GlobalAlumniKeyMap(), +} class ExternalCourseSyncAPIJobStatus(Enum): """ @@ -543,7 +544,7 @@ def create_or_update_external_course_page(course_index_page, course, external_co course=course, title=external_course.course_title, external_marketing_url=external_course.marketing_url, - subhead=keymap.COURSE_PAGE_SUBHEAD, + subhead=keymap.course_page_subhead, duration=external_course.duration, format=external_course.format, description=external_course.description, @@ -684,7 +685,7 @@ def create_learning_outcomes_page(course_page, outcomes_list, keymap): learning_outcome_page = LearningOutcomesPage( heading=keymap.LEARNING_OUTCOMES_PAGE_HEADING, - sub_heading=keymap.LEARNING_OUTCOMES_PAGE_SUBHEAD, + sub_heading=keymap.learning_outcomes_page_subhead, outcome_items=outcome_items, ) course_page.add_child(instance=learning_outcome_page) diff --git a/courses/sync_external_courses/external_course_sync_api_test.py b/courses/sync_external_courses/external_course_sync_api_test.py index b373cbbe3..53a9801d1 100644 --- a/courses/sync_external_courses/external_course_sync_api_test.py +++ b/courses/sync_external_courses/external_course_sync_api_test.py @@ -23,7 +23,6 @@ from courses.sync_external_courses.external_course_sync_api import ( ExternalCourse, EmeritusKeyMap, - EMERITUS_PLATFORM_NAME, create_learning_outcomes_page, create_or_update_certificate_page, create_or_update_external_course_page, @@ -452,7 +451,7 @@ def test_update_external_course_runs( # noqa: PLR0915 ).open() as test_data_file: external_course_runs = json.load(test_data_file)["rows"] - platform = PlatformFactory.create(name=EMERITUS_PLATFORM_NAME) + platform = PlatformFactory.create(name="Emeritus") if create_existing_data: for run in random.sample(external_course_runs, len(external_course_runs) // 2): @@ -677,7 +676,7 @@ def test_create_or_update_product_and_product_version( # noqa: PLR0913 """ external_course_data["list_price"] = new_price external_course = ExternalCourse(external_course_data, keymap=EmeritusKeyMap()) - platform = PlatformFactory.create(name=EMERITUS_PLATFORM_NAME) + platform = PlatformFactory.create(name="Emeritus") course = CourseFactory.create( external_course_id=external_course.course_code, platform=platform, diff --git a/courses/tasks.py b/courses/tasks.py index 1ebcb13a2..9c7aed881 100644 --- a/courses/tasks.py +++ b/courses/tasks.py @@ -9,8 +9,9 @@ from django.db.models import Q from requests.exceptions import HTTPError -from courses.models import CourseRun, CourseRunCertificate -from courses.sync_external_courses.emeritus_api import ( +from courses.models import CourseRun, CourseRunCertificate, Platform +from courses.sync_external_courses.external_course_sync_api import ( + VENDOR_KEYMAPS, fetch_external_courses, update_external_course_runs, ) @@ -114,11 +115,20 @@ def sync_courseruns_data(): @app.task -def task_sync_emeritus_course_runs(): +def task_sync_external_course_runs(): """Task to sync Emeritus course runs""" if not settings.FEATURES.get("ENABLE_EXTERNAL_COURSE_SYNC", False): log.info("External Course sync is disabled.") return - emeritus_course_runs = fetch_external_courses() - update_external_course_runs(emeritus_course_runs) + platforms = Platform.objects.filter(sync_daily=True) + for platform in platforms: + keymap = VENDOR_KEYMAPS.get(platform.name.lower()) + if not keymap: + log.exception( + "The platform '%s' does not have a sync API configured. Please disable the 'sync_daily' setting for this platform.", + platform.name, + ) + continue + emeritus_course_runs = fetch_external_courses(keymap) + update_external_course_runs(emeritus_course_runs, keymap) diff --git a/courses/tasks_test.py b/courses/tasks_test.py index dab9a664e..2e7a69dd3 100644 --- a/courses/tasks_test.py +++ b/courses/tasks_test.py @@ -4,8 +4,8 @@ import pytest -from courses.factories import CourseRunFactory -from courses.tasks import sync_courseruns_data, task_sync_emeritus_course_runs +from courses.factories import CourseRunFactory, PlatformFactory +from courses.tasks import sync_courseruns_data, task_sync_external_course_runs pytestmark = [pytest.mark.django_db] @@ -25,13 +25,25 @@ def test_sync_courseruns_data(mocker): assert Counter(actual_course_runs) == Counter(course_runs) -def test_task_sync_emeritus_course_runs(mocker, settings): - """Test task_sync_emeritus_course_runs calls the right api functionality""" +def test_task_sync_external_course_runs(mocker, settings): + """Test task_sync_external_course_runs skips platforms not in VENDOR_KEYMAPS""" settings.FEATURES["ENABLE_EXTERNAL_COURSE_SYNC"] = True + mock_fetch_external_courses = mocker.patch("courses.tasks.fetch_external_courses") mock_update_external_course_runs = mocker.patch( "courses.tasks.update_external_course_runs" ) - task_sync_emeritus_course_runs.delay() + log_mock = mocker.patch("courses.tasks.log") + + PlatformFactory.create(name="Emeritus", sync_daily=True) + PlatformFactory.create(name="UnknownPlatform", sync_daily=True) + + task_sync_external_course_runs.delay() + mock_fetch_external_courses.assert_called_once() mock_update_external_course_runs.assert_called_once() + + log_mock.exception.assert_called_once_with( + "The platform '%s' does not have a sync API configured. Please disable the 'sync_daily' setting for this platform.", + "UnknownPlatform", + ) \ No newline at end of file diff --git a/mitxpro/settings.py b/mitxpro/settings.py index f67064fca..87f1fde84 100644 --- a/mitxpro/settings.py +++ b/mitxpro/settings.py @@ -761,14 +761,14 @@ ) CRON_EXTERNAL_COURSERUN_SYNC_HOURS = get_string( - name="CRON_EMERITUS_COURSERUN_SYNC_HOURS", + name="CRON_EXTERNAL_COURSERUN_SYNC_HOURS", default="0", - description="'hours' value for the 'sync-emeritus-course-runs' scheduled task (defaults to midnight)", + description="'hours' value for the 'sync-external-course-runs' scheduled task (defaults to midnight)", ) CRON_EXTERNAL_COURSERUN_SYNC_DAYS = get_string( - name="CRON_EMERITUS_COURSERUN_SYNC_DAYS", + name="CRON_EXTERNAL_COURSERUN_SYNC_DAYS", default=None, - description="'day_of_week' value for 'sync-emeritus-course-runs' scheduled task (default will run once a day).", + description="'day_of_week' value for 'sync-external-course-runs' scheduled task (default will run once a day).", ) CRON_BASKET_DELETE_HOURS = get_string( @@ -885,8 +885,8 @@ month_of_year="*", ), }, - "sync-emeritus-course-runs": { - "task": "courses.tasks.task_sync_emeritus_course_runs", + "sync-external-course-runs": { + "task": "courses.tasks.task_sync_external_course_runs", "schedule": crontab( minute="0", hour=CRON_EXTERNAL_COURSERUN_SYNC_HOURS, @@ -1131,18 +1131,18 @@ EXTERNAL_COURSE_SYNC_API_KEY = get_string( name="EXTERNAL_COURSE_SYNC_API_KEY", default=None, - description="The API Key for Emeritus API", + description="The API Key for external course sync API", required=True, ) EXTERNAL_COURSE_SYNC_API_BASE_URL = get_string( name="EXTERNAL_COURSE_SYNC_API_BASE_URL", default="https://mit-xpro.emeritus-analytics.io/", - description="Base API URL for Emeritus API", + description="Base API URL for external course sync API", ) EXTERNAL_COURSE_SYNC_API_REQUEST_TIMEOUT = get_int( - name="EMERITUS_API_TIMEOUT", + name="EXTERNAL_COURSE_SYNC_API_REQUEST_TIMEOUT", default=60, - description="API request timeout for Emeritus APIs in seconds", + description="API request timeout for external course sync APIs in seconds", ) # django debug toolbar only in debug mode