From 54e117cb142bdfb4bc798647e1821d20504ad8c2 Mon Sep 17 00:00:00 2001 From: MaxFrank13 Date: Thu, 25 Sep 2025 18:28:09 +0000 Subject: [PATCH 1/7] feat: program dash endpoint b2c --- .../programs/rest_api/v1/tests/test_views.py | 93 ++++++++++++++++++- .../djangoapps/programs/rest_api/v1/urls.py | 4 + .../djangoapps/programs/rest_api/v1/views.py | 12 ++- 3 files changed, 103 insertions(+), 6 deletions(-) diff --git a/openedx/core/djangoapps/programs/rest_api/v1/tests/test_views.py b/openedx/core/djangoapps/programs/rest_api/v1/tests/test_views.py index 2864f41a9216..71277c988c02 100644 --- a/openedx/core/djangoapps/programs/rest_api/v1/tests/test_views.py +++ b/openedx/core/djangoapps/programs/rest_api/v1/tests/test_views.py @@ -8,6 +8,7 @@ from django.core.cache import cache from django.test.utils import override_settings from django.urls import reverse_lazy +from common.djangoapps.student.models import CourseEnrollment from enterprise.models import EnterpriseCourseEnrollment from common.djangoapps.student.tests.factories import ( @@ -144,7 +145,7 @@ def test_404_if_no_program_data(self, mock_get_programs, _mock_get_pathways): @skip_unless_lms -class TestProgramsView(SharedModuleStoreTestCase, ProgramCacheMixin): +class TestProgramsEnterpriseView(SharedModuleStoreTestCase, ProgramCacheMixin): """Unit tests for the program details page.""" enterprise_uuid = str(uuid4()) @@ -196,7 +197,7 @@ def setUp(self): @with_site_configuration(configuration={"COURSE_CATALOG_API_URL": "foo"}) @override_settings(FEATURES=dict(ENABLE_ENTERPRISE_INTEGRATION=True)) @enterprise_is_enabled() - def test_program_list(self): + def test_program_list_enterprise(self): """ Verify API returns proper response. """ @@ -243,3 +244,91 @@ def test_program_empty_list_if_no_enterprise_enrollments(self): response = self.client.get(self.url) self.assertEqual(response.status_code, 200) self.assertEqual(response.data, []) + +@skip_unless_lms +class TestProgramsB2CView(SharedModuleStoreTestCase, ProgramCacheMixin): + """Unit tests for the program details page.""" + + program_uuid = str(uuid4()) + url = reverse_lazy("openedx.core.djangoapps.programs:v0:program_list_B2C") + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.user = UserFactory() + modulestore_course = ModuleStoreCourseFactory() + course_run = CourseRunFactory(key=str(modulestore_course.id)) + course = CourseFactory(course_runs=[course_run]) + + CourseEnrollmentFactory(is_active=True, course_id=modulestore_course.id, user=cls.user) + + cls.program = ProgramFactory( + uuid=cls.program_uuid, + courses=[course], + title="Journey to cooking", + type="MicroMasters", + authoring_organizations=[ + { + "key": "MAX", + "logo_image_url": "http://test.org/media/organization/logos/test-logo.png", + } + ], + ) + cls.site = SiteFactory(domain="test.localhost") + + def setUp(self): + super().setUp() + self.client.login(username=self.user.username, password=self.TEST_PASSWORD) + self.set_program_in_catalog_cache(self.program_uuid, self.program) + ProgramEnrollmentFactory.create( + user=self.user, + program_uuid=self.program_uuid, + external_user_key="0001", + ) + + @with_site_configuration(configuration={"COURSE_CATALOG_API_URL": "foo"}) + def test_program_list_b2c(self): + """ + Verify API returns proper response. + """ + cache.set( + SITE_PROGRAM_UUIDS_CACHE_KEY_TPL.format(domain=self.site.domain), + [self.program_uuid], + None, + ) + + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + program = response.data[0] + + assert len(program) + assert program["uuid"] == self.program["uuid"] + assert program["title"] == self.program["title"] + assert program["type"] == self.program["type"] + assert program["authoring_organizations"] == self.program["authoring_organizations"] + assert program["banner_image"] == self.program["banner_image"] + assert program["progress"] == { + "uuid": self.program["uuid"], + "completed": 0, + "in_progress": 0, + "not_started": 1, + "all_unenrolled": False, + } + + @with_site_configuration(configuration={"COURSE_CATALOG_API_URL": "foo"}) + def test_program_empty_list_if_no_enrollments(self): + """ + Verify API returns empty response if no enrollments exists for a learner. + """ + CourseEnrollment.objects.filter(user=self.user).delete() + + cache.set( + SITE_PROGRAM_UUIDS_CACHE_KEY_TPL.format(domain=self.site.domain), + [self.program_uuid], + None, + ) + + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, []) \ No newline at end of file diff --git a/openedx/core/djangoapps/programs/rest_api/v1/urls.py b/openedx/core/djangoapps/programs/rest_api/v1/urls.py index 415a543a9280..54b6b2492646 100644 --- a/openedx/core/djangoapps/programs/rest_api/v1/urls.py +++ b/openedx/core/djangoapps/programs/rest_api/v1/urls.py @@ -18,6 +18,10 @@ Programs.as_view(), name="program_list", ), + re_path(rf"^programs/$", + Programs.as_view(), + name="program_list_B2C", + ), re_path( rf"^programs/(?P{PROGRAM_UUID_PATTERN})/progress_details/$", ProgramProgressDetailView.as_view(), diff --git a/openedx/core/djangoapps/programs/rest_api/v1/views.py b/openedx/core/djangoapps/programs/rest_api/v1/views.py index a5bf939e1ef6..3546f16f8312 100644 --- a/openedx/core/djangoapps/programs/rest_api/v1/views.py +++ b/openedx/core/djangoapps/programs/rest_api/v1/views.py @@ -33,8 +33,8 @@ class Programs(APIView): permission_classes = (IsAuthenticated,) - def get(self, request: "HttpRequest", enterprise_uuid: str) -> "HttpResponse": - """For an enterprise learner, get list of enrolled programs with progress. + def get(self, request: "HttpRequest", enterprise_uuid: str = None) -> "HttpResponse": + """For a learner, get list of enrolled programs with progress. If an enterprise UUID ias provided, filter out all non-enterprise enrollments for the learner. **Example Request** @@ -89,8 +89,12 @@ def get(self, request: "HttpRequest", enterprise_uuid: str) -> "HttpResponse": """ user: "AnonymousUser | User" = request.user - enrollments = list(self._get_enterprise_course_enrollments(enterprise_uuid, user)) - # return empty reponse if no enterprise enrollments exists for a user + if enterprise_uuid is not None: + enrollments = list(self._get_enterprise_course_enrollments(enterprise_uuid, user)) + else: + enrollments = list(get_course_enrollments(user)) + + # return empty reponse if no enrollments exists for a user if not enrollments: return Response([]) From 7523455a3cd5524e6991a0b88f8256bae7cebb2a Mon Sep 17 00:00:00 2001 From: MaxFrank13 Date: Thu, 25 Sep 2025 19:44:01 +0000 Subject: [PATCH 2/7] fix: lint --- .../core/djangoapps/programs/rest_api/v1/tests/test_views.py | 4 ++-- openedx/core/djangoapps/programs/rest_api/v1/urls.py | 2 +- openedx/core/djangoapps/programs/rest_api/v1/views.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openedx/core/djangoapps/programs/rest_api/v1/tests/test_views.py b/openedx/core/djangoapps/programs/rest_api/v1/tests/test_views.py index 71277c988c02..188064e086ed 100644 --- a/openedx/core/djangoapps/programs/rest_api/v1/tests/test_views.py +++ b/openedx/core/djangoapps/programs/rest_api/v1/tests/test_views.py @@ -8,9 +8,9 @@ from django.core.cache import cache from django.test.utils import override_settings from django.urls import reverse_lazy -from common.djangoapps.student.models import CourseEnrollment from enterprise.models import EnterpriseCourseEnrollment +from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.student.tests.factories import ( CourseEnrollmentFactory, UserFactory, @@ -331,4 +331,4 @@ def test_program_empty_list_if_no_enrollments(self): response = self.client.get(self.url) self.assertEqual(response.status_code, 200) - self.assertEqual(response.data, []) \ No newline at end of file + self.assertEqual(response.data, []) diff --git a/openedx/core/djangoapps/programs/rest_api/v1/urls.py b/openedx/core/djangoapps/programs/rest_api/v1/urls.py index 54b6b2492646..029fc3115548 100644 --- a/openedx/core/djangoapps/programs/rest_api/v1/urls.py +++ b/openedx/core/djangoapps/programs/rest_api/v1/urls.py @@ -20,7 +20,7 @@ ), re_path(rf"^programs/$", Programs.as_view(), - name="program_list_B2C", + name="program_list_b2c", ), re_path( rf"^programs/(?P{PROGRAM_UUID_PATTERN})/progress_details/$", diff --git a/openedx/core/djangoapps/programs/rest_api/v1/views.py b/openedx/core/djangoapps/programs/rest_api/v1/views.py index 3546f16f8312..61123f74e3a9 100644 --- a/openedx/core/djangoapps/programs/rest_api/v1/views.py +++ b/openedx/core/djangoapps/programs/rest_api/v1/views.py @@ -33,7 +33,7 @@ class Programs(APIView): permission_classes = (IsAuthenticated,) - def get(self, request: "HttpRequest", enterprise_uuid: str = None) -> "HttpResponse": + def get(self, request: "HttpRequest", enterprise_uuid: Optional[str] = None) -> "HttpResponse": """For a learner, get list of enrolled programs with progress. If an enterprise UUID ias provided, filter out all non-enterprise enrollments for the learner. **Example Request** @@ -89,7 +89,7 @@ def get(self, request: "HttpRequest", enterprise_uuid: str = None) -> "HttpRespo """ user: "AnonymousUser | User" = request.user - if enterprise_uuid is not None: + if enterprise_uuid: enrollments = list(self._get_enterprise_course_enrollments(enterprise_uuid, user)) else: enrollments = list(get_course_enrollments(user)) From 92cc09e8d5a089fb0db08a9fa4b0717927b2f4a4 Mon Sep 17 00:00:00 2001 From: MaxFrank13 Date: Thu, 25 Sep 2025 19:59:23 +0000 Subject: [PATCH 3/7] fix: lint --- .../djangoapps/programs/rest_api/v1/tests/test_views.py | 1 + openedx/core/djangoapps/programs/rest_api/v1/urls.py | 3 ++- openedx/core/djangoapps/programs/rest_api/v1/views.py | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openedx/core/djangoapps/programs/rest_api/v1/tests/test_views.py b/openedx/core/djangoapps/programs/rest_api/v1/tests/test_views.py index 188064e086ed..26b0aeb70f4a 100644 --- a/openedx/core/djangoapps/programs/rest_api/v1/tests/test_views.py +++ b/openedx/core/djangoapps/programs/rest_api/v1/tests/test_views.py @@ -245,6 +245,7 @@ def test_program_empty_list_if_no_enterprise_enrollments(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.data, []) + @skip_unless_lms class TestProgramsB2CView(SharedModuleStoreTestCase, ProgramCacheMixin): """Unit tests for the program details page.""" diff --git a/openedx/core/djangoapps/programs/rest_api/v1/urls.py b/openedx/core/djangoapps/programs/rest_api/v1/urls.py index 029fc3115548..b5abfa89a03b 100644 --- a/openedx/core/djangoapps/programs/rest_api/v1/urls.py +++ b/openedx/core/djangoapps/programs/rest_api/v1/urls.py @@ -18,7 +18,8 @@ Programs.as_view(), name="program_list", ), - re_path(rf"^programs/$", + re_path( + rf"^programs/$", Programs.as_view(), name="program_list_b2c", ), diff --git a/openedx/core/djangoapps/programs/rest_api/v1/views.py b/openedx/core/djangoapps/programs/rest_api/v1/views.py index 61123f74e3a9..dd290369c771 100644 --- a/openedx/core/djangoapps/programs/rest_api/v1/views.py +++ b/openedx/core/djangoapps/programs/rest_api/v1/views.py @@ -90,10 +90,10 @@ def get(self, request: "HttpRequest", enterprise_uuid: Optional[str] = None) -> user: "AnonymousUser | User" = request.user if enterprise_uuid: - enrollments = list(self._get_enterprise_course_enrollments(enterprise_uuid, user)) + enrollments = list(self._get_enterprise_course_enrollments(enterprise_uuid, user)) else: - enrollments = list(get_course_enrollments(user)) - + enrollments = list(get_course_enrollments(user)) + # return empty reponse if no enrollments exists for a user if not enrollments: return Response([]) From edb2a59d14a9409f47010701b2059fc3bc3c7474 Mon Sep 17 00:00:00 2001 From: MaxFrank13 Date: Thu, 25 Sep 2025 20:19:04 +0000 Subject: [PATCH 4/7] fix: lint --- openedx/core/djangoapps/programs/rest_api/v1/urls.py | 2 +- openedx/core/djangoapps/programs/rest_api/v1/views.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openedx/core/djangoapps/programs/rest_api/v1/urls.py b/openedx/core/djangoapps/programs/rest_api/v1/urls.py index b5abfa89a03b..f6ee9628cfb4 100644 --- a/openedx/core/djangoapps/programs/rest_api/v1/urls.py +++ b/openedx/core/djangoapps/programs/rest_api/v1/urls.py @@ -19,7 +19,7 @@ name="program_list", ), re_path( - rf"^programs/$", + "^programs/$", Programs.as_view(), name="program_list_b2c", ), diff --git a/openedx/core/djangoapps/programs/rest_api/v1/views.py b/openedx/core/djangoapps/programs/rest_api/v1/views.py index dd290369c771..2b4ba00b76e4 100644 --- a/openedx/core/djangoapps/programs/rest_api/v1/views.py +++ b/openedx/core/djangoapps/programs/rest_api/v1/views.py @@ -33,8 +33,9 @@ class Programs(APIView): permission_classes = (IsAuthenticated,) - def get(self, request: "HttpRequest", enterprise_uuid: Optional[str] = None) -> "HttpResponse": - """For a learner, get list of enrolled programs with progress. If an enterprise UUID ias provided, filter out all non-enterprise enrollments for the learner. + def get(self, request: "HttpRequest", enterprise_uuid: str = None) -> "HttpResponse": + """For a learner, get list of enrolled programs with progress. + If an enterprise UUID ias provided, filter out all non-enterprise enrollments for the learner. **Example Request** From 162593a75ed64ea0f31f4e5003846a1488d4c77f Mon Sep 17 00:00:00 2001 From: MaxFrank13 Date: Thu, 25 Sep 2025 20:26:15 +0000 Subject: [PATCH 5/7] fix: lint --- openedx/core/djangoapps/programs/rest_api/v1/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/programs/rest_api/v1/views.py b/openedx/core/djangoapps/programs/rest_api/v1/views.py index 2b4ba00b76e4..9e42774bde4e 100644 --- a/openedx/core/djangoapps/programs/rest_api/v1/views.py +++ b/openedx/core/djangoapps/programs/rest_api/v1/views.py @@ -34,7 +34,7 @@ class Programs(APIView): permission_classes = (IsAuthenticated,) def get(self, request: "HttpRequest", enterprise_uuid: str = None) -> "HttpResponse": - """For a learner, get list of enrolled programs with progress. + """For a learner, get list of enrolled programs with progress. If an enterprise UUID ias provided, filter out all non-enterprise enrollments for the learner. **Example Request** From d995053ee3bad4ab3d2c46a773ccf0bee01629e1 Mon Sep 17 00:00:00 2001 From: MaxFrank13 Date: Thu, 25 Sep 2025 20:31:44 +0000 Subject: [PATCH 6/7] fix: default to empty string --- openedx/core/djangoapps/programs/rest_api/v1/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/programs/rest_api/v1/views.py b/openedx/core/djangoapps/programs/rest_api/v1/views.py index 9e42774bde4e..77c5a171fd7f 100644 --- a/openedx/core/djangoapps/programs/rest_api/v1/views.py +++ b/openedx/core/djangoapps/programs/rest_api/v1/views.py @@ -33,7 +33,7 @@ class Programs(APIView): permission_classes = (IsAuthenticated,) - def get(self, request: "HttpRequest", enterprise_uuid: str = None) -> "HttpResponse": + def get(self, request: "HttpRequest", enterprise_uuid: str = "") -> "HttpResponse": """For a learner, get list of enrolled programs with progress. If an enterprise UUID ias provided, filter out all non-enterprise enrollments for the learner. From dbd2ff975d7770e253ba46b0ba22f62ecb6edd69 Mon Sep 17 00:00:00 2001 From: MaxFrank13 Date: Thu, 25 Sep 2025 21:55:02 +0000 Subject: [PATCH 7/7] fix: test --- .../core/djangoapps/programs/rest_api/v1/tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/programs/rest_api/v1/tests/test_views.py b/openedx/core/djangoapps/programs/rest_api/v1/tests/test_views.py index 26b0aeb70f4a..11428591db74 100644 --- a/openedx/core/djangoapps/programs/rest_api/v1/tests/test_views.py +++ b/openedx/core/djangoapps/programs/rest_api/v1/tests/test_views.py @@ -251,7 +251,7 @@ class TestProgramsB2CView(SharedModuleStoreTestCase, ProgramCacheMixin): """Unit tests for the program details page.""" program_uuid = str(uuid4()) - url = reverse_lazy("openedx.core.djangoapps.programs:v0:program_list_B2C") + url = reverse_lazy("openedx.core.djangoapps.programs:v0:program_list_b2c") @classmethod def setUpClass(cls):