Skip to content

Commit d09c364

Browse files
Anas12091101asadali145
andauthoredDec 12, 2024··
feat: add Global Alumni in external course sync (#3330)
* feat: add Global Alumni in external course sync * refactor: made code generic and adjusted tests * refactor: added sync_daily in platform and refactored courses/task.py to be more generic * fix: test * fix: some issues * fix: some issues * fix: some issues * fix: issues after rebase * fix: some issues * fix: some issues * fix: pre-commit issues * fix: issues * fix: tests * test: add GA tests * fix: issues * fix: tests * fix: tests --------- Co-authored-by: Asad Ali <[email protected]>
1 parent a19d3e8 commit d09c364

18 files changed

+721
-454
lines changed
 

‎.env.example

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ HUBSPOT_ID_PREFIX=
4040
MITOL_DIGITAL_CREDENTIALS_VERIFY_SERVICE_BASE_URL=http://host.docker.internal:5000/
4141
MITOL_DIGITAL_CREDENTIALS_HMAC_SECRET=test-hmac-secret # pragma: allowlist secret
4242

43-
EMERITUS_API_KEY=fake_api_key
43+
EXTERNAL_COURSE_SYNC_API_KEY=fake_api_key
4444

4545
POSTHOG_PROJECT_API_KEY=
4646
POSTHOG_API_HOST=https://app.posthog.com/

‎.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ jobs:
117117
DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres # pragma: allowlist secret
118118
WEBPACK_DISABLE_LOADER_STATS: "True"
119119
ELASTICSEARCH_URL: localhost:9200
120-
EMERITUS_API_KEY: fake_emeritus_api_key # pragma: allowlist secret
120+
EXTERNAL_COURSE_SYNC_API_KEY: fake_external_course_sync_api_key # pragma: allowlist secret
121121
MAILGUN_KEY: fake_mailgun_key
122122
MAILGUN_SENDER_DOMAIN: other.fake.site
123123
MITOL_DIGITAL_CREDENTIALS_VERIFY_SERVICE_BASE_URL: http://localhost:5000

‎app.json

+14-14
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,12 @@
9898
"description": "'hours' value for the 'generate-course-certificate' scheduled task (defaults to midnight)",
9999
"required": false
100100
},
101-
"CRON_EMERITUS_COURSERUN_SYNC_DAYS": {
102-
"description": "'day_of_week' value for 'sync-emeritus-course-runs' scheduled task (default will run once a day).",
101+
"CRON_EXTERNAL_COURSERUN_SYNC_DAYS": {
102+
"description": "'day_of_week' value for 'sync-external-course-runs' scheduled task (default will run once a day).",
103103
"required": false
104104
},
105-
"CRON_EMERITUS_COURSERUN_SYNC_HOURS": {
106-
"description": "'hours' value for the 'sync-emeritus-course-runs' scheduled task (defaults to midnight)",
105+
"CRON_EXTERNAL_COURSERUN_SYNC_HOURS": {
106+
"description": "'hours' value for the 'sync-external-course-runs' scheduled task (defaults to midnight)",
107107
"required": false
108108
},
109109
"CSRF_TRUSTED_ORIGINS": {
@@ -234,20 +234,20 @@
234234
"description": "Timeout (in seconds) for requests made via the edX API client",
235235
"required": false
236236
},
237-
"EMERITUS_API_BASE_URL": {
238-
"description": "Base API URL for Emeritus API",
237+
"ENROLLMENT_CHANGE_SHEET_ID": {
238+
"description": "ID of the Google Sheet that contains the enrollment change request worksheets (refunds, transfers, etc)",
239239
"required": false
240240
},
241-
"EMERITUS_API_KEY": {
242-
"description": "The API Key for Emeritus API",
243-
"required": true
244-
},
245-
"EMERITUS_API_TIMEOUT": {
246-
"description": "API request timeout for Emeritus APIs in seconds",
241+
"EXTERNAL_COURSE_SYNC_API_BASE_URL": {
242+
"description": "Base API URL for external course sync API",
247243
"required": false
248244
},
249-
"ENROLLMENT_CHANGE_SHEET_ID": {
250-
"description": "ID of the Google Sheet that contains the enrollment change request worksheets (refunds, transfers, etc)",
245+
"EXTERNAL_COURSE_SYNC_API_KEY": {
246+
"description": "The API Key for external course sync API",
247+
"required": true
248+
},
249+
"EXTERNAL_COURSE_SYNC_API_REQUEST_TIMEOUT": {
250+
"description": "API request timeout for external course sync APIs in seconds",
251251
"required": false
252252
},
253253
"GA_TRACKING_ID": {

‎courses/management/commands/sync_external_course_runs.py

+16-15
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
from django.core.management.base import BaseCommand
44

5-
from courses.sync_external_courses.emeritus_api import (
6-
EmeritusKeyMap,
7-
fetch_emeritus_courses,
8-
update_emeritus_course_runs,
5+
from courses.sync_external_courses.external_course_sync_api import (
6+
EXTERNAL_COURSE_VENDOR_KEYMAPS,
7+
fetch_external_courses,
8+
update_external_course_runs,
99
)
1010
from mitxpro import settings
1111

@@ -36,18 +36,19 @@ def handle(self, *args, **options): # noqa: ARG002
3636
return
3737

3838
vendor_name = options["vendor_name"]
39-
if vendor_name.lower() == EmeritusKeyMap.PLATFORM_NAME.value.lower():
40-
self.stdout.write(f"Starting course sync for {vendor_name}.")
41-
emeritus_course_runs = fetch_emeritus_courses()
42-
stats = update_emeritus_course_runs(emeritus_course_runs)
43-
self.log_stats(stats)
44-
self.stdout.write(
45-
self.style.SUCCESS(
46-
f"External course sync successful for {vendor_name}."
47-
)
48-
)
49-
else:
39+
keymap = EXTERNAL_COURSE_VENDOR_KEYMAPS.get(vendor_name.lower())
40+
if not keymap:
5041
self.stdout.write(self.style.ERROR(f"Unknown vendor name {vendor_name}."))
42+
return
43+
44+
self.stdout.write(f"Starting course sync for {vendor_name}.")
45+
keymap = keymap()
46+
external_course_runs = fetch_external_courses(keymap)
47+
stats = update_external_course_runs(external_course_runs, keymap)
48+
self.log_stats(stats)
49+
self.stdout.write(
50+
self.style.SUCCESS(f"External course sync successful for {vendor_name}.")
51+
)
5152

5253
def log_stats(self, stats):
5354
"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 4.2.16 on 2024-12-06 13:47
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("courses", "0040_alter_courserun_courseware_id"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="platform",
14+
name="sync_daily",
15+
field=models.BooleanField(
16+
default=False,
17+
help_text="Select this option to enable daily syncing for external course platforms.",
18+
),
19+
),
20+
]

‎courses/models.py

+4
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ class Platform(TimestampedModel, ValidateOnSaveMixin):
214214
"""
215215

216216
name = models.CharField(max_length=255, unique=True)
217+
sync_daily = models.BooleanField(
218+
default=False,
219+
help_text="Select this option to enable daily syncing for external course platforms.",
220+
)
217221

218222
def __str__(self):
219223
return self.name

‎courses/models_test.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
ProgramRunFactory,
2727
)
2828
from courses.models import CourseRunEnrollment, limit_to_certificate_pages
29+
from courses.sync_external_courses.external_course_sync_api import (
30+
EMERITUS_PLATFORM_NAME,
31+
)
2932
from ecommerce.factories import ProductFactory, ProductVersionFactory
3033
from mitxpro.test_utils import format_as_iso8601
3134
from mitxpro.utils import now_in_utc
@@ -812,7 +815,7 @@ def test_platform_name_is_unique():
812815
"""
813816
Tests that case-insensitive platform name is unique.
814817
"""
815-
PlatformFactory.create(name="Emeritus")
818+
PlatformFactory.create(name=EMERITUS_PLATFORM_NAME)
816819

817820
with pytest.raises(ValidationError):
818-
PlatformFactory.create(name="emeritus")
821+
PlatformFactory.create(name=EMERITUS_PLATFORM_NAME.lower())

‎courses/sync_external_courses/emeritus_api.py ‎courses/sync_external_courses/external_course_sync_api.py

+226-175
Large diffs are not rendered by default.

‎courses/sync_external_courses/emeritus_api_client.py ‎courses/sync_external_courses/external_course_sync_api_client.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
API client for Emeritus
2+
External course sync API client
33
"""
44

55
import json
@@ -8,15 +8,15 @@
88
from django.conf import settings
99

1010

11-
class EmeritusAPIClient:
11+
class ExternalCourseSyncAPIClient:
1212
"""
13-
API client for Emeritus
13+
External course sync API client
1414
"""
1515

1616
def __init__(self):
17-
self.api_key = settings.EMERITUS_API_KEY
18-
self.base_url = settings.EMERITUS_API_BASE_URL
19-
self.request_timeout = settings.EMERITUS_API_REQUEST_TIMEOUT
17+
self.api_key = settings.EXTERNAL_COURSE_SYNC_API_KEY
18+
self.base_url = settings.EXTERNAL_COURSE_SYNC_API_BASE_URL
19+
self.request_timeout = settings.EXTERNAL_COURSE_SYNC_API_REQUEST_TIMEOUT
2020

2121
def get_queries_list(self):
2222
"""

‎courses/sync_external_courses/emeritus_api_client_test.py ‎courses/sync_external_courses/external_course_sync_api_client_test.py

+25-19
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
"""
2-
Tests for emeritus_api_client
2+
Tests for external_course_sync_api_client
33
"""
44

55
import json
66
from datetime import timedelta
77

88
import pytest
99

10-
from courses.sync_external_courses.emeritus_api_client import EmeritusAPIClient
10+
from courses.sync_external_courses.external_course_sync_api_client import (
11+
ExternalCourseSyncAPIClient,
12+
)
1113
from mitxpro.test_utils import MockResponse
1214
from mitxpro.utils import now_in_utc
1315

@@ -22,7 +24,7 @@
2224
),
2325
[
2426
(
25-
"courses.sync_external_courses.emeritus_api_client.requests.get",
27+
"courses.sync_external_courses.external_course_sync_api_client.requests.get",
2628
MockResponse(
2729
{
2830
"results": [
@@ -35,25 +37,25 @@
3537
),
3638
"get_queries_list",
3739
[],
38-
"https://test-emeritus-api.io/api/queries?api_key=test_emeritus_api_key",
40+
"https://test-external-course-sync-api.io/api/queries?api_key=test_external_course_sync_api_key",
3941
),
4042
(
41-
"courses.sync_external_courses.emeritus_api_client.requests.get",
43+
"courses.sync_external_courses.external_course_sync_api_client.requests.get",
4244
MockResponse({"job": {"status": 1}}),
4345
"get_job_status",
4446
[12],
45-
"https://test-emeritus-api.io/api/jobs/12?api_key=test_emeritus_api_key",
47+
"https://test-external-course-sync-api.io/api/jobs/12?api_key=test_external_course_sync_api_key",
4648
),
4749
(
48-
"courses.sync_external_courses.emeritus_api_client.requests.get",
50+
"courses.sync_external_courses.external_course_sync_api_client.requests.get",
4951
MockResponse({"query_result": {"data": {}}}),
5052
"get_query_result",
5153
[20],
52-
"https://test-emeritus-api.io/api/query_results/20?api_key=test_emeritus_api_key",
54+
"https://test-external-course-sync-api.io/api/query_results/20?api_key=test_external_course_sync_api_key",
5355
),
5456
],
5557
)
56-
def test_emeritus_api_client_get_requests( # noqa: PLR0913
58+
def test_external_course_sync_api_client_get_requests( # noqa: PLR0913
5759
mocker,
5860
settings,
5961
patch_request_path,
@@ -62,14 +64,16 @@ def test_emeritus_api_client_get_requests( # noqa: PLR0913
6264
args,
6365
expected_api_url,
6466
):
65-
settings.EMERITUS_API_KEY = "test_emeritus_api_key"
66-
settings.EMERITUS_API_BASE_URL = "https://test-emeritus-api.io"
67-
settings.EMERITUS_API_REQUEST_TIMEOUT = 60
67+
settings.EXTERNAL_COURSE_SYNC_API_KEY = "test_external_course_sync_api_key"
68+
settings.EXTERNAL_COURSE_SYNC_API_BASE_URL = (
69+
"https://test-external-course-sync-api.io"
70+
)
71+
settings.EXTERNAL_COURSE_SYNC_API_REQUEST_TIMEOUT = 60
6872

6973
mock_get = mocker.patch(patch_request_path)
7074
mock_get.return_value = mock_response
7175

72-
client = EmeritusAPIClient()
76+
client = ExternalCourseSyncAPIClient()
7377
client_method_map = {
7478
"get_queries_list": client.get_queries_list,
7579
"get_job_status": client.get_job_status,
@@ -84,23 +88,25 @@ def test_emeritus_api_client_get_requests( # noqa: PLR0913
8488

8589
def test_get_query_response(mocker, settings):
8690
"""
87-
Tests that `EmeritusAPIClient.get_query_response` makes the expected post request.
91+
Tests that `ExternalCourseSyncAPIClient.get_query_response` makes the expected post request.
8892
"""
8993
end_date = now_in_utc()
9094
start_date = end_date - timedelta(days=1)
9195

92-
settings.EMERITUS_API_KEY = "test_emeritus_api_key"
93-
settings.EMERITUS_API_BASE_URL = "https://test-emeritus-api.io"
96+
settings.EXTERNAL_COURSE_SYNC_API_KEY = "test_external_course_sync_api_key"
97+
settings.EXTERNAL_COURSE_SYNC_API_BASE_URL = (
98+
"https://test-external-course-sync-api.io"
99+
)
94100

95101
mock_post = mocker.patch(
96-
"courses.sync_external_courses.emeritus_api_client.requests.post"
102+
"courses.sync_external_courses.external_course_sync_api_client.requests.post"
97103
)
98104
mock_post.return_value = MockResponse({"job": {"id": 1}})
99105

100-
client = EmeritusAPIClient()
106+
client = ExternalCourseSyncAPIClient()
101107
client.get_query_response(1, start_date, end_date)
102108
mock_post.assert_called_once_with(
103-
"https://test-emeritus-api.io/api/queries/1/results?api_key=test_emeritus_api_key",
109+
"https://test-external-course-sync-api.io/api/queries/1/results?api_key=test_external_course_sync_api_key",
104110
data=json.dumps(
105111
{
106112
"parameters": {

‎courses/sync_external_courses/emeritus_api_test.py ‎courses/sync_external_courses/external_course_sync_api_test.py

+296-167
Large diffs are not rendered by default.

‎courses/sync_external_courses/test_data/batch_test.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@
114114
"language": "English",
115115
"image_name": "test_emeritus_image.jpg",
116116
"ceu": "2.8",
117-
"landing_page_url": "https://test-emeritus-api.io/Internet-of-things-iot-design-and-applications?utm_medium=EmWebsite&utm_campaign=direct_EmWebsite?utm_campaign=school_website&utm_medium=website&utm_source=MIT-web",
118-
"Apply_now_url": "https://test-emeritus-api.io/?locale=en&program_sfid=01t2s000000OHA2AAO&source=applynowlp&utm_campaign=school&utm_medium=MITWebsite&utm_source=MIT-web",
117+
"landing_page_url": "https://test-external-course-sync-api.io/Internet-of-things-iot-design-and-applications?utm_medium=EmWebsite&utm_campaign=direct_EmWebsite?utm_campaign=school_website&utm_medium=website&utm_source=MIT-web",
118+
"Apply_now_url": "https://test-external-course-sync-api.io/?locale=en&program_sfid=01t2s000000OHA2AAO&source=applynowlp&utm_campaign=school&utm_medium=MITWebsite&utm_source=MIT-web",
119119
"description": "By 2030, McKinsey Digital research estimates that it could enable USD 5.5 trillion to USD 12.6 trillion in value globally, including the value captured by consumers and customers of Internet of Things ( IoT) products and services. This future dominated by IoT requires a paradigm shift in the way products and services are designed. MIT xPRO’s Internet of Things (IoT): Design and Applications program is designed to provide you with a strategic road map for creating user-centered, future-ready, differentiated products that drive business growth. Through activities, assignments, and industry examples, you will learn the fundamental principles of developing IoT-centric products and services.\r",
120120
"learning_outcomes": "This program will enable you to:\r\n● Understand how IoT mindsets are contributing to a global shift in product and business strategy\r\n● Discover how IoT impacts the design of both products and businesses\r\n● Identify hardware, software, and data technologies that support IoT adoption\r\n● Explore examples of successful IoT product and business strategies\r\n● Examine legal, ethical, privacy, and security concerns related to IoT",
121121
"program_for": "The program is ideal for:\r\n● Managers and functional leaders looking to learn the strategies for successfully leveraging IoT principles to drive business value\r\n● Product managers and designers who want to shift to an IoT-centric mindset to develop innovative products and services\r\n● Technology professionals keen on understanding the fundamental principles associated with the implementation of IoT and its wide array of applications\r\nNote: This is a nontechnical program for which there are no prerequisites.\r"
@@ -183,8 +183,8 @@
183183
"language": "English",
184184
"image_name": "test_emeritus_image.jpg",
185185
"ceu": "0.8",
186-
"landing_page_url": "https://test-emeritus-api.io/professional-certificate-cybersecurity?utm_campaign=school_website&utm_medium=website&utm_source=MIT-web",
187-
"Apply_now_url": "https://test-emeritus-api.io/?locale=en&program_sfid=01t2s000000ZdQKAA0&source=applynowlp&utm_campaign=school&utm_medium=MITWebsite&utm_source=MIT-web",
186+
"landing_page_url": "https://test-external-course-sync-api.io/professional-certificate-cybersecurity?utm_campaign=school_website&utm_medium=website&utm_source=MIT-web",
187+
"Apply_now_url": "https://test-external-course-sync-api.io/?locale=en&program_sfid=01t2s000000ZdQKAA0&source=applynowlp&utm_campaign=school&utm_medium=MITWebsite&utm_source=MIT-web",
188188
"description": "Cyberattacks are becoming more frequent, complex, and targeted, collectively costing organizations billions of dollars annually. It’s no wonder that cybersecurity is one of the fastest growing industries; by 2027, Forbes projects the value of the cybersecurity market to reach USD 403 billion. More and more companies and government agencies are seeking to hire cybersecurity professionals with the specialized technical skills needed to defend mission-critical computer systems, networks, and cloud applications against cyberattacks. If you’re keen to step into this high-growth field and advance your career, the MIT xPRO Professional Certificate in Cybersecurity is for you.\r",
189189
"learning_outcomes": "This program will enable you to:\r\n● Gain an overview of cybersecurity risk management, including its foundational concepts and relevant regulations\r\n● Explore the domains covering various aspects of cloud technology\r\n● Learn adversary tactics and techniques that are utilized as the foundational development of specific threat models and methodologies\r\n● Understand the guidelines for organizations to prepare themselves against cybersecurity attacks",
190190
"program_for": "The program is ideal for:\r\n● Early-career IT professionals, network engineers, and system administrators wanting to gain a comprehensive overview of cybersecurity and fast-track their career progression\r\n● IT project managers and engineers keen on gaining the ability to think critically about the threat landscape, including vulnerabilities in cybersecurity, and upgrading their resume for career advancement\r\n● Mid- or later-career professionals seeking a career change and looking to add critical cybersecurity knowledge and foundational lessons to their resume"

‎courses/tasks.py

+22-8
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
from django.db.models import Q
1010
from requests.exceptions import HTTPError
1111

12-
from courses.models import CourseRun, CourseRunCertificate
13-
from courses.sync_external_courses.emeritus_api import (
14-
fetch_emeritus_courses,
15-
update_emeritus_course_runs,
12+
from courses.models import CourseRun, CourseRunCertificate, Platform
13+
from courses.sync_external_courses.external_course_sync_api import (
14+
EXTERNAL_COURSE_VENDOR_KEYMAPS,
15+
fetch_external_courses,
16+
update_external_course_runs,
1617
)
1718
from courses.utils import (
1819
ensure_course_run_grade,
@@ -114,11 +115,24 @@ def sync_courseruns_data():
114115

115116

116117
@app.task
117-
def task_sync_emeritus_course_runs():
118-
"""Task to sync Emeritus course runs"""
118+
def task_sync_external_course_runs():
119+
"""Task to sync external course runs"""
119120
if not settings.FEATURES.get("ENABLE_EXTERNAL_COURSE_SYNC", False):
120121
log.info("External Course sync is disabled.")
121122
return
122123

123-
emeritus_course_runs = fetch_emeritus_courses()
124-
update_emeritus_course_runs(emeritus_course_runs)
124+
platforms = Platform.objects.filter(sync_daily=True)
125+
for platform in platforms:
126+
keymap = EXTERNAL_COURSE_VENDOR_KEYMAPS.get(platform.name.lower())
127+
if not keymap:
128+
log.exception(
129+
"The platform '%s' does not have a sync API configured. Please disable the 'sync_daily' setting for this platform.",
130+
platform.name,
131+
)
132+
continue
133+
try:
134+
keymap = keymap()
135+
external_course_runs = fetch_external_courses(keymap)
136+
update_external_course_runs(external_course_runs, keymap)
137+
except Exception:
138+
log.exception("Some error occurred")

‎courses/tasks_test.py

+25-10
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
import pytest
66

7-
from courses.factories import CourseRunFactory
8-
from courses.tasks import sync_courseruns_data, task_sync_emeritus_course_runs
7+
from courses.factories import CourseRunFactory, PlatformFactory
8+
from courses.sync_external_courses.external_course_sync_api import (
9+
EMERITUS_PLATFORM_NAME,
10+
)
11+
from courses.tasks import sync_courseruns_data, task_sync_external_course_runs
912

1013
pytestmark = [pytest.mark.django_db]
1114

@@ -25,13 +28,25 @@ def test_sync_courseruns_data(mocker):
2528
assert Counter(actual_course_runs) == Counter(course_runs)
2629

2730

28-
def test_task_sync_emeritus_course_runs(mocker, settings):
29-
"""Test task_sync_emeritus_course_runs calls the right api functionality"""
31+
def test_task_sync_external_course_runs(mocker, settings):
32+
"""Test task_sync_external_course_runs to call APIs for supported platforms and skip unsupported ones in EXTERNAL_COURSE_VENDOR_KEYMAPS"""
3033
settings.FEATURES["ENABLE_EXTERNAL_COURSE_SYNC"] = True
31-
mock_fetch_emeritus_courses = mocker.patch("courses.tasks.fetch_emeritus_courses")
32-
mock_update_emeritus_course_runs = mocker.patch(
33-
"courses.tasks.update_emeritus_course_runs"
34+
35+
mock_fetch_external_courses = mocker.patch("courses.tasks.fetch_external_courses")
36+
mock_update_external_course_runs = mocker.patch(
37+
"courses.tasks.update_external_course_runs"
38+
)
39+
mock_log = mocker.patch("courses.tasks.log")
40+
41+
PlatformFactory.create(name=EMERITUS_PLATFORM_NAME, sync_daily=True)
42+
PlatformFactory.create(name="UnknownPlatform", sync_daily=True)
43+
44+
task_sync_external_course_runs.delay()
45+
46+
mock_fetch_external_courses.assert_called_once()
47+
mock_update_external_course_runs.assert_called_once()
48+
49+
mock_log.exception.assert_called_once_with(
50+
"The platform '%s' does not have a sync API configured. Please disable the 'sync_daily' setting for this platform.",
51+
"UnknownPlatform",
3452
)
35-
task_sync_emeritus_course_runs.delay()
36-
mock_fetch_emeritus_courses.assert_called_once()
37-
mock_update_emeritus_course_runs.assert_called_once()

‎courses/urls.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from rest_framework import routers
55

66
from courses.views import v1
7-
from courses.views.v1 import EmeritusCourseListView
7+
from courses.views.v1 import ExternalCourseListView
88

99
router = routers.SimpleRouter()
1010
router.register(r"programs", v1.ProgramViewSet, basename="programs_api")
@@ -31,8 +31,8 @@
3131
r"^api/enrollments/", v1.UserEnrollmentsView.as_view(), name="user-enrollments"
3232
),
3333
path(
34-
"api/emeritus_courses/",
35-
EmeritusCourseListView.as_view(),
36-
name="emeritus_courses",
34+
"api/external_courses/<str:vendor>/",
35+
ExternalCourseListView.as_view(),
36+
name="external_courses",
3737
),
3838
]

‎courses/views/v1/__init__.py

+18-5
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@
2727
ProgramEnrollmentSerializer,
2828
ProgramSerializer,
2929
)
30-
from courses.sync_external_courses.emeritus_api import fetch_emeritus_courses
30+
from courses.sync_external_courses.external_course_sync_api import (
31+
EXTERNAL_COURSE_VENDOR_KEYMAPS,
32+
fetch_external_courses,
33+
)
3134
from ecommerce.models import Product
3235

3336

@@ -205,19 +208,29 @@ def get_queryset(self):
205208
return CourseTopic.parent_topics_with_courses()
206209

207210

208-
class EmeritusCourseListView(APIView):
211+
class ExternalCourseListView(APIView):
209212
"""
210-
ReadOnly View to list Emeritus courses.
213+
ReadOnly View to list External courses.
211214
"""
212215

213216
permission_classes = [IsAdminUser]
214217

215218
def get(self, request, *args, **kwargs): # noqa: ARG002
216219
"""
217-
Get Emeritus courses list from the Emeritus API and return it.
220+
Get External courses list from the External API and return it.
218221
"""
222+
223+
vendor = kwargs.get("vendor").replace("_", " ")
224+
keymap = EXTERNAL_COURSE_VENDOR_KEYMAPS.get(vendor.lower())
225+
if not keymap:
226+
return Response(
227+
{
228+
"error": f"The vendor '{vendor}' is not supported. Supported vendors are {', '.join(EXTERNAL_COURSE_VENDOR_KEYMAPS)}"
229+
},
230+
status=status.HTTP_400_BAD_REQUEST,
231+
)
219232
try:
220-
data = fetch_emeritus_courses()
233+
data = fetch_external_courses(keymap())
221234
return Response(data, status=status.HTTP_200_OK)
222235
except Exception as e: # noqa: BLE001
223236
return Response(

‎courses/views_test.py

+19-8
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
ProgramCertificateSerializer,
3838
ProgramSerializer,
3939
)
40+
from courses.sync_external_courses.external_course_sync_api import (
41+
EMERITUS_PLATFORM_NAME,
42+
GLOBAL_ALUMNI_PLATFORM_NAME,
43+
)
4044
from ecommerce.factories import ProductFactory, ProductVersionFactory
4145
from mitxpro.test_utils import assert_drf_json_equal
4246
from mitxpro.utils import now_in_utc
@@ -616,30 +620,37 @@ def test_course_topics_api(client, django_assert_num_queries):
616620

617621

618622
@pytest.mark.parametrize("expected_status_code", [200, 500])
619-
def test_emeritus_course_list_view(admin_drf_client, mocker, expected_status_code):
623+
@pytest.mark.parametrize(
624+
"vendor_name", [EMERITUS_PLATFORM_NAME, GLOBAL_ALUMNI_PLATFORM_NAME]
625+
)
626+
def test_external_course_list_view(
627+
admin_drf_client, mocker, expected_status_code, vendor_name
628+
):
620629
"""
621-
Test that the Emeritus API List calls fetch_emeritus_courses and returns its mocked response.
630+
Test that the External API List calls fetch_external_courses and returns its mocked response.
622631
"""
623632
if expected_status_code == 200:
624633
with Path(
625634
"courses/sync_external_courses/test_data/batch_test.json"
626635
).open() as test_data_file:
627636
mocked_response = json.load(test_data_file)["rows"]
628637

629-
patched_fetch_emeritus_courses = mocker.patch(
630-
"courses.views.v1.fetch_emeritus_courses", return_value=mocked_response
638+
patched_fetch_external_courses = mocker.patch(
639+
"courses.views.v1.fetch_external_courses", return_value=mocked_response
631640
)
632641
else:
633-
patched_fetch_emeritus_courses = mocker.patch(
634-
"courses.views.v1.fetch_emeritus_courses",
642+
patched_fetch_external_courses = mocker.patch(
643+
"courses.views.v1.fetch_external_courses",
635644
side_effect=Exception("Some error occurred."),
636645
)
637646
mocked_response = {
638647
"error": "Some error occurred.",
639648
"details": "Some error occurred.",
640649
}
641650

642-
response = admin_drf_client.get(reverse("emeritus_courses"))
651+
response = admin_drf_client.get(
652+
reverse("external_courses", kwargs={"vendor": vendor_name})
653+
)
643654
assert response.json() == mocked_response
644655
assert response.status_code == expected_status_code
645-
patched_fetch_emeritus_courses.assert_called_once()
656+
patched_fetch_external_courses.assert_called_once()

‎mitxpro/settings.py

+15-15
Original file line numberDiff line numberDiff line change
@@ -761,14 +761,14 @@
761761
)
762762

763763
CRON_EXTERNAL_COURSERUN_SYNC_HOURS = get_string(
764-
name="CRON_EMERITUS_COURSERUN_SYNC_HOURS",
764+
name="CRON_EXTERNAL_COURSERUN_SYNC_HOURS",
765765
default="0",
766-
description="'hours' value for the 'sync-emeritus-course-runs' scheduled task (defaults to midnight)",
766+
description="'hours' value for the 'sync-external-course-runs' scheduled task (defaults to midnight)",
767767
)
768768
CRON_EXTERNAL_COURSERUN_SYNC_DAYS = get_string(
769-
name="CRON_EMERITUS_COURSERUN_SYNC_DAYS",
769+
name="CRON_EXTERNAL_COURSERUN_SYNC_DAYS",
770770
default=None,
771-
description="'day_of_week' value for 'sync-emeritus-course-runs' scheduled task (default will run once a day).",
771+
description="'day_of_week' value for 'sync-external-course-runs' scheduled task (default will run once a day).",
772772
)
773773

774774
CRON_BASKET_DELETE_HOURS = get_string(
@@ -885,8 +885,8 @@
885885
month_of_year="*",
886886
),
887887
},
888-
"sync-emeritus-course-runs": {
889-
"task": "courses.tasks.task_sync_emeritus_course_runs",
888+
"sync-external-course-runs": {
889+
"task": "courses.tasks.task_sync_external_course_runs",
890890
"schedule": crontab(
891891
minute="0",
892892
hour=CRON_EXTERNAL_COURSERUN_SYNC_HOURS,
@@ -1139,21 +1139,21 @@
11391139
description="Timeout (in seconds) for requests made via the edX API client",
11401140
)
11411141

1142-
EMERITUS_API_KEY = get_string(
1143-
name="EMERITUS_API_KEY",
1142+
EXTERNAL_COURSE_SYNC_API_KEY = get_string(
1143+
name="EXTERNAL_COURSE_SYNC_API_KEY",
11441144
default=None,
1145-
description="The API Key for Emeritus API",
1145+
description="The API Key for external course sync API",
11461146
required=True,
11471147
)
1148-
EMERITUS_API_BASE_URL = get_string(
1149-
name="EMERITUS_API_BASE_URL",
1148+
EXTERNAL_COURSE_SYNC_API_BASE_URL = get_string(
1149+
name="EXTERNAL_COURSE_SYNC_API_BASE_URL",
11501150
default="https://mit-xpro.emeritus-analytics.io/",
1151-
description="Base API URL for Emeritus API",
1151+
description="Base API URL for external course sync API",
11521152
)
1153-
EMERITUS_API_REQUEST_TIMEOUT = get_int(
1154-
name="EMERITUS_API_TIMEOUT",
1153+
EXTERNAL_COURSE_SYNC_API_REQUEST_TIMEOUT = get_int(
1154+
name="EXTERNAL_COURSE_SYNC_API_REQUEST_TIMEOUT",
11551155
default=60,
1156-
description="API request timeout for Emeritus APIs in seconds",
1156+
description="API request timeout for external course sync APIs in seconds",
11571157
)
11581158

11591159
# django debug toolbar only in debug mode

0 commit comments

Comments
 (0)
Please sign in to comment.