diff --git a/lms/djangoapps/learner_home/serializers.py b/lms/djangoapps/learner_home/serializers.py index f4cce6e5eb..f9073d9ecc 100644 --- a/lms/djangoapps/learner_home/serializers.py +++ b/lms/djangoapps/learner_home/serializers.py @@ -6,9 +6,11 @@ from urllib.parse import urljoin from django.conf import settings from django.urls import reverse +from opaque_keys.edx.keys import CourseKey from rest_framework import serializers from common.djangoapps.course_modes.models import CourseMode +from common.djangoapps.student.helpers import user_has_passing_grade_in_course from openedx.features.course_experience import course_home_url from xmodule.data import CertificatesDisplayBehaviors @@ -17,6 +19,7 @@ class LiteralField(serializers.Field): """ Custom Field for use with fields that will always intentionally serialize to the same static value. """ + def __init__(self, literal_value): super().__init__() self.literal_value = literal_value @@ -37,9 +40,9 @@ class PlatformSettingsSerializer(serializers.Serializer): class CourseProviderSerializer(serializers.Serializer): - """Info about a course provider (institution/business)""" + """Info about a course provider (institution/business) from a CourseOverview""" - name = serializers.CharField() + name = serializers.CharField(source="display_org_with_default") class CourseSerializer(serializers.Serializer): @@ -223,7 +226,10 @@ class EnrollmentSerializer(serializers.Serializer): class GradeDataSerializer(serializers.Serializer): """Info about grades for this enrollment""" - isPassing = serializers.BooleanField() + isPassing = serializers.SerializerMethodField() + + def get_isPassing(self, enrollment): + return user_has_passing_grade_in_course(enrollment) class CertificateSerializer(serializers.Serializer): @@ -287,9 +293,9 @@ class CertificateSerializer(serializers.Serializer): class AvailableEntitlementSessionSerializer(serializers.Serializer): """An available entitlement session""" - startDate = serializers.DateTimeField(source='start') - endDate = serializers.DateTimeField(source='end') - courseId = serializers.CharField(source='key') + startDate = serializers.DateTimeField(source="start") + endDate = serializers.DateTimeField(source="end") + courseId = serializers.CharField(source="key") class EntitlementSerializer(serializers.Serializer): @@ -298,7 +304,7 @@ class EntitlementSerializer(serializers.Serializer): availableSessions = serializers.SerializerMethodField() uuid = serializers.UUIDField() - isRefundable = serializers.BooleanField(source='is_entitlement_refundable') + isRefundable = serializers.BooleanField(source="is_entitlement_refundable") isFulfilled = serializers.SerializerMethodField() changeDeadline = serializers.SerializerMethodField() isExpired = serializers.SerializerMethodField() @@ -314,8 +320,10 @@ class EntitlementSerializer(serializers.Serializer): return bool(instance.expired_at) def get_availableSessions(self, instance): - avaialableSessions = self.context['course_entitlement_available_sessions'].get(str(instance.uuid)) - return AvailableEntitlementSessionSerializer(avaialableSessions, many=True).data + availableSessions = self.context["course_entitlement_available_sessions"].get( + str(instance.uuid) + ) + return AvailableEntitlementSessionSerializer(availableSessions, many=True).data def get_expirationDate(self, instance): if instance.expired_at is not None: @@ -327,7 +335,7 @@ class EntitlementSerializer(serializers.Serializer): return self.get_expirationDate(instance) def get_enrollmentUrl(self, instance): - return reverse('entitlements_api:v1:enrollments', args=[str(instance.uuid)]) + return reverse("entitlements_api:v1:enrollments", args=[str(instance.uuid)]) class RelatedProgramSerializer(serializers.Serializer): @@ -356,24 +364,27 @@ class LearnerEnrollmentSerializer(serializers.Serializer): Info for displaying an enrollment on the learner dashboard. Derived from a CourseEnrollment with added context. """ + requires_context = True course = CourseSerializer() + courseProvider = CourseProviderSerializer(source="course_overview") courseRun = CourseRunSerializer(source="*") enrollment = EnrollmentSerializer(source="*") certificate = CertificateSerializer(source="*") entitlement = serializers.SerializerMethodField() + gradeData = GradeDataSerializer(source="*") # TODO - remove "allow_null" as each of these are implemented, temp for testing. - courseProvider = CourseProviderSerializer(allow_null=True) - gradeData = GradeDataSerializer(allow_null=True) programs = ProgramsSerializer(allow_null=True) def get_entitlement(self, instance): """ If this enrollment is the fulfillment of an entitlement, include information about the entitlement """ - entitlement = self.context['fulfilled_entitlements'].get(str(instance.course_id)) + entitlement = self.context["fulfilled_entitlements"].get( + str(instance.course_id) + ) if entitlement: return EntitlementSerializer(entitlement, context=self.context).data else: @@ -391,23 +402,24 @@ class UnfulfilledEntitlementSerializer(serializers.Serializer): # This is the static constant data returned as the 'enrollment' key for all unfulfilled enrollments. STATIC_ENTITLEMENT_ENROLLMENT_DATA = { - 'accessExpirationDate': None, - 'isAudit': False, - 'hasStarted': False, - 'hasAccess': True, - 'isVerified': False, - 'canUpgrade': False, - 'isAuditAccessExpired': False, - 'isEmailEnabled': False, - 'hasOptedOutOfEmail': False, - 'lastEnrolled': None, - 'isEnrolled': False, + "accessExpirationDate": None, + "isAudit": False, + "hasStarted": False, + "hasAccess": True, + "isVerified": False, + "canUpgrade": False, + "isAuditAccessExpired": False, + "isEmailEnabled": False, + "hasOptedOutOfEmail": False, + "lastEnrolled": None, + "isEnrolled": False, } class _PseudoSessionCourseSerializer(serializers.Serializer): """ - 'Private' Serilizer for the 'course' key data. This data comes from the pseudo session + 'Private' Serializer for the 'course' key data. This data comes from the pseudo session """ + bannerImgSrc = serializers.URLField(source="image.src") courseName = serializers.CharField(source="title") courseNumber = serializers.CharField(source="key") @@ -415,9 +427,9 @@ class UnfulfilledEntitlementSerializer(serializers.Serializer): # These fields contain all real data and will be serialized entitlement = EntitlementSerializer(source="*") course = serializers.SerializerMethodField() + courseProvider = serializers.SerializerMethodField() # Change after data is implemented. This data is required - courseProvider = CourseProviderSerializer(allow_null=True) programs = ProgramsSerializer(allow_null=True) # These fields are literal values that do not change @@ -427,8 +439,27 @@ class UnfulfilledEntitlementSerializer(serializers.Serializer): enrollment = LiteralField(STATIC_ENTITLEMENT_ENROLLMENT_DATA) def get_course(self, instance): - pseudo_session = self.context['unfulfilled_entitlement_pseudo_sessions'].get(str(instance.uuid)) - return UnfulfilledEntitlementSerializer._PseudoSessionCourseSerializer(pseudo_session).data + pseudo_session = self.context["unfulfilled_entitlement_pseudo_sessions"].get( + str(instance.uuid) + ) + return UnfulfilledEntitlementSerializer._PseudoSessionCourseSerializer( + pseudo_session + ).data + + def get_courseProvider(self, entitlement): + """Look up course provider from CourseOverview matching the pseudo session""" + pseudo_session = self.context["unfulfilled_entitlement_pseudo_sessions"].get( + str(entitlement.uuid) + ) + course_overview = None + + if pseudo_session: + course_key = CourseKey.from_string(pseudo_session["key"]) + course_overview = self.context.get("pseudo_session_course_overviews").get( + course_key + ) + + return CourseProviderSerializer(course_overview, allow_null=True).data class SuggestedCourseSerializer(serializers.Serializer): @@ -450,11 +481,11 @@ class EmailConfirmationSerializer(serializers.Serializer): class EnterpriseDashboardSerializer(serializers.Serializer): """Serializer for individual enterprise dashboard data""" - label = serializers.CharField(source='name') + label = serializers.CharField(source="name") url = serializers.SerializerMethodField() def get_url(self, instance): - return urljoin(settings.ENTERPRISE_LEARNER_PORTAL_BASE_URL, instance['uuid']) + return urljoin(settings.ENTERPRISE_LEARNER_PORTAL_BASE_URL, instance["uuid"]) class LearnerDashboardSerializer(serializers.Serializer): diff --git a/lms/djangoapps/learner_home/test_serializers.py b/lms/djangoapps/learner_home/test_serializers.py index a7cd1f8e57..976a3e549e 100644 --- a/lms/djangoapps/learner_home/test_serializers.py +++ b/lms/djangoapps/learner_home/test_serializers.py @@ -9,6 +9,8 @@ from uuid import uuid4 from django.conf import settings from django.test import TestCase import ddt +from opaque_keys.edx.keys import CourseKey + from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.course_modes.tests.factories import CourseModeFactory @@ -17,7 +19,12 @@ from common.djangoapps.student.tests.factories import ( CourseEnrollmentFactory, UserFactory, ) -from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory as CatalogCourseRunFactory +from openedx.core.djangoapps.catalog.tests.factories import ( + CourseRunFactory as CatalogCourseRunFactory, +) +from openedx.core.djangoapps.content.course_overviews.tests.factories import ( + CourseOverviewFactory, +) from lms.djangoapps.learner_home.serializers import ( CertificateSerializer, CourseProviderSerializer, @@ -72,6 +79,31 @@ class LearnerDashboardBaseTest(SharedModuleStoreTestCase): return test_enrollment + def create_test_entitlement_and_sessions(self): + """ + Create a test entitlement + + Returns: (unfulfilled_entitlement, pseudo_sessions, available_sessions) + """ + unfulfilled_entitlement = CourseEntitlementFactory.create() + + # Create pseudo-sessions + pseudo_sessions = { + str(unfulfilled_entitlement.uuid): CatalogCourseRunFactory.create() + } + + # Create available sessions + available_sessions = { + str(unfulfilled_entitlement.uuid): CatalogCourseRunFactory.create_batch(3) + } + + # Create related course overviews + course_key_str = pseudo_sessions[str(unfulfilled_entitlement.uuid)]["key"] + course_key = CourseKey.from_string(course_key_str) + course_overview = CourseOverviewFactory.create(id=course_key) + + return unfulfilled_entitlement, pseudo_sessions, available_sessions + def _assert_all_keys_equal(self, dicts): element_0 = dicts[0] for element in dicts[1:]: @@ -102,7 +134,7 @@ class TestPlatformSettingsSerializer(TestCase): } -class TestCourseProviderSerializer(TestCase): +class TestCourseProviderSerializer(LearnerDashboardBaseTest): """Tests for the CourseProviderSerializer""" @classmethod @@ -113,12 +145,12 @@ class TestCourseProviderSerializer(TestCase): } def test_happy_path(self): - input_data = self.generate_test_provider_info() + test_enrollment = self.create_test_enrollment() + + input_data = test_enrollment.course_overview output_data = CourseProviderSerializer(input_data).data - assert output_data == { - "name": input_data["name"], - } + self.assertEqual(output_data["name"], test_enrollment.course_overview.org) class TestCourseSerializer(LearnerDashboardBaseTest): @@ -344,23 +376,29 @@ class TestEnrollmentSerializer(LearnerDashboardBaseTest): self.assertFalse(output["hasStarted"]) -class TestGradeDataSerializer(TestCase): +@ddt.ddt +class TestGradeDataSerializer(LearnerDashboardBaseTest): """Tests for the GradeDataSerializer""" - @classmethod - def generate_test_grade_data(cls): - """Util to generate test grade data""" - return { - "isPassing": random_bool(), - } + @mock.patch( + "lms.djangoapps.learner_home.serializers.user_has_passing_grade_in_course" + ) + @ddt.data(True, False, None) + def test_happy_path(self, is_passing, mock_get_grade_data): + # Given a course where I am/not passing + input_data = self.create_test_enrollment() + mock_get_grade_data.return_value = is_passing - def test_happy_path(self): - input_data = self.generate_test_grade_data() + # When I serialize grade data output_data = GradeDataSerializer(input_data).data - assert output_data == { - "isPassing": input_data["isPassing"], - } + # Then I get the correct data shape out + self.assertDictEqual( + output_data, + { + "isPassing": is_passing, + }, + ) @ddt.ddt @@ -581,13 +619,13 @@ class TestCertificateSerializer(LearnerDashboardBaseTest): class TestEntitlementSerializer(TestCase): """Tests for the EntitlementSerializer""" - def _assert_availale_sessions(self, input_sessions, output_sessions): + def _assert_available_sessions(self, input_sessions, output_sessions): assert len(output_sessions) == len(input_sessions) for input_session, output_session in zip(input_sessions, output_sessions): assert output_session == { - 'startDate': input_session['start'], - 'endDate': input_session['end'], - 'courseId': input_session['key'] + "startDate": input_session["start"], + "endDate": input_session["end"], + "courseId": input_session["key"], } @ddt.unpack @@ -595,26 +633,33 @@ class TestEntitlementSerializer(TestCase): def test_serialize_entitlement(self, isExpired, isEnrolled): entitlement_kwargs = {} if isExpired: - entitlement_kwargs['expired_at'] = datetime.now() + entitlement_kwargs["expired_at"] = datetime.now() if isEnrolled: - entitlement_kwargs['enrollment_course_run'] = CourseEnrollmentFactory.create() + entitlement_kwargs[ + "enrollment_course_run" + ] = CourseEnrollmentFactory.create() entitlement = CourseEntitlementFactory.create(**entitlement_kwargs) available_sessions = CatalogCourseRunFactory.create_batch(4) course_entitlement_available_sessions = { str(entitlement.uuid): available_sessions } - output_data = EntitlementSerializer(entitlement, context={ - 'course_entitlement_available_sessions': course_entitlement_available_sessions - }).data + output_data = EntitlementSerializer( + entitlement, + context={ + "course_entitlement_available_sessions": course_entitlement_available_sessions + }, + ).data - output_sessions = output_data.pop('availableSessions') - self._assert_availale_sessions(available_sessions, output_sessions) + output_sessions = output_data.pop("availableSessions") + self._assert_available_sessions(available_sessions, output_sessions) if isExpired: expected_expiration_date = entitlement.expired_at else: - expected_expiration_date = date.today() + timedelta(days=entitlement.get_days_until_expiration()) + expected_expiration_date = date.today() + timedelta( + days=entitlement.get_days_until_expiration() + ) assert output_data == { "isRefundable": entitlement.is_entitlement_refundable(), @@ -623,7 +668,7 @@ class TestEntitlementSerializer(TestCase): "isExpired": bool(entitlement.expired_at), "expirationDate": expected_expiration_date, "uuid": str(entitlement.uuid), - "enrollmentUrl": f"/api/entitlements/v1/entitlements/{entitlement.uuid}/enrollments" + "enrollmentUrl": f"/api/entitlements/v1/entitlements/{entitlement.uuid}/enrollments", } @@ -736,14 +781,29 @@ class TestUnfulfilledEntitlementSerializer(LearnerDashboardBaseTest): def test_happy_path(self): """Test that nothing breaks and the output fields look correct""" unfulfilled_entitlement = CourseEntitlementFactory.create() - pseudo_sessions = {str(unfulfilled_entitlement.uuid): CatalogCourseRunFactory.create()} - available_sessions = {str(unfulfilled_entitlement.uuid): CatalogCourseRunFactory.create_batch(3)} - context = { - 'unfulfilled_entitlement_pseudo_sessions': pseudo_sessions, - 'course_entitlement_available_sessions': available_sessions, + pseudo_sessions = { + str(unfulfilled_entitlement.uuid): CatalogCourseRunFactory.create() + } + available_sessions = { + str(unfulfilled_entitlement.uuid): CatalogCourseRunFactory.create_batch(3) } - output_data = UnfulfilledEntitlementSerializer(unfulfilled_entitlement, context=context).data + # create course overview for course provider info + course_key_str = pseudo_sessions[str(unfulfilled_entitlement.uuid)]["key"] + course_key = CourseKey.from_string(course_key_str) + course_overview = CourseOverviewFactory.create(id=course_key) + + pseudo_session_course_overviews = {course_key: course_overview} + + context = { + "unfulfilled_entitlement_pseudo_sessions": pseudo_sessions, + "course_entitlement_available_sessions": available_sessions, + "pseudo_session_course_overviews": pseudo_session_course_overviews, + } + + output_data = UnfulfilledEntitlementSerializer( + unfulfilled_entitlement, context=context + ).data expected_keys = [ "courseProvider", @@ -753,14 +813,18 @@ class TestUnfulfilledEntitlementSerializer(LearnerDashboardBaseTest): "courseRun", "gradeData", "certificate", - "enrollment" + "enrollment", ] assert output_data.keys() == set(expected_keys) - assert output_data['courseRun'] is None - assert output_data['gradeData'] is None - assert output_data['certificate'] is None - assert output_data['enrollment'] == UnfulfilledEntitlementSerializer.STATIC_ENTITLEMENT_ENROLLMENT_DATA + assert output_data["courseProvider"] is not None + assert output_data["courseRun"] is None + assert output_data["gradeData"] is None + assert output_data["certificate"] is None + assert ( + output_data["enrollment"] + == UnfulfilledEntitlementSerializer.STATIC_ENTITLEMENT_ENROLLMENT_DATA + ) def test_static_enrollment_data(self): """ @@ -768,7 +832,9 @@ class TestUnfulfilledEntitlementSerializer(LearnerDashboardBaseTest): This test is to ensure that that dict has the same keys as returned by the LearnerEnrollmentSerializer """ output_data = TestEnrollmentSerializer().serialize_test_enrollment() - expected_keys = UnfulfilledEntitlementSerializer.STATIC_ENTITLEMENT_ENROLLMENT_DATA.keys() + expected_keys = ( + UnfulfilledEntitlementSerializer.STATIC_ENTITLEMENT_ENROLLMENT_DATA.keys() + ) actual_keys = output_data.keys() assert expected_keys == actual_keys @@ -888,7 +954,9 @@ class TestEnterpriseDashboardSerializer(TestCase): output_data, { "label": input_data["name"], - "url": settings.ENTERPRISE_LEARNER_PORTAL_BASE_URL + '/' + input_data["uuid"], + "url": settings.ENTERPRISE_LEARNER_PORTAL_BASE_URL + + "/" + + input_data["uuid"], }, ) @@ -899,9 +967,14 @@ class TestLearnerDashboardSerializer(LearnerDashboardBaseTest): # Show full diff for serialization issues maxDiff = None - def make_test_context(self, enrollments=None, enrollments_with_entitlements=None, unfulfilled_entitlements=None): + def make_test_context( + self, + enrollments=None, + enrollments_with_entitlements=None, + unfulfilled_entitlements=None, + ): """ - Given enrollments and entitlements, generate a mathing serializer context + Given enrollments and entitlements, generate a matching serializer context """ enrollments = enrollments or [] enrollments_with_entitlements = enrollments_with_entitlements or [] @@ -938,6 +1011,17 @@ class TestLearnerDashboardSerializer(LearnerDashboardBaseTest): for entitlement in all_entitlements } + # Create related course overviews for entitlement pseudo sessions + pseudo_session_course_overviews = {} + for unfulfilled_entitlement in unfulfilled_entitlement_pseudo_sessions: + course_key_str = unfulfilled_entitlement_pseudo_sessions[ + unfulfilled_entitlement + ]["key"] + course_key = CourseKey.from_string(course_key_str) + course_overview = CourseOverviewFactory.create(id=course_key) + + pseudo_session_course_overviews[course_key] = course_overview + input_context = { "resume_course_urls": resume_course_urls, "ecommerce_payment_page": random_url(), @@ -945,6 +1029,7 @@ class TestLearnerDashboardSerializer(LearnerDashboardBaseTest): "fulfilled_entitlements": fulfilled_entitlements, "unfulfilled_entitlement_pseudo_sessions": unfulfilled_entitlement_pseudo_sessions, "course_entitlement_available_sessions": course_entitlement_available_sessions, + "pseudo_session_course_overviews": pseudo_session_course_overviews, } return input_context @@ -998,10 +1083,7 @@ class TestLearnerDashboardSerializer(LearnerDashboardBaseTest): def test_entitlements(self): # One standard enrollment, one fulfilled entitlement, one unfulfilled enrollment - enrollments = [ - self.create_test_enrollment(), - self.create_test_enrollment() - ] + enrollments = [self.create_test_enrollment(), self.create_test_enrollment()] unfulfilled_entitlements = [CourseEntitlementFactory.create()] input_context = self.make_test_context( @@ -1027,7 +1109,7 @@ class TestLearnerDashboardSerializer(LearnerDashboardBaseTest): self._assert_all_keys_equal(courses) # Non-entitlement enrollment should have no entitlement info assert not courses[0]['entitlement'] - # Fulfuilled and Unfulfilled entitlement should have identical keys + # Fulfilled and Unfulfilled entitlement should have identical keys fulfilled_entitlement = courses[1]['entitlement'] unfulfilled_entitlement = courses[2]['entitlement'] assert fulfilled_entitlement diff --git a/lms/djangoapps/learner_home/test_views.py b/lms/djangoapps/learner_home/test_views.py index 4d2b0812ec..424d2c7a19 100644 --- a/lms/djangoapps/learner_home/test_views.py +++ b/lms/djangoapps/learner_home/test_views.py @@ -10,9 +10,9 @@ import ddt from django.conf import settings from django.urls import reverse from django.utils import timezone +from opaque_keys.edx.keys import CourseKey from rest_framework.test import APITestCase -from lms.djangoapps.learner_home.test_utils import create_test_enrollment from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.entitlements.tests.factories import CourseEntitlementFactory from common.djangoapps.student.tests.factories import ( @@ -20,7 +20,9 @@ from common.djangoapps.student.tests.factories import ( UserFactory, ) from lms.djangoapps.bulk_email.models import Optout +from lms.djangoapps.learner_home.test_utils import create_test_enrollment from lms.djangoapps.learner_home.views import ( + get_course_overviews_for_pseudo_sessions, get_email_settings_info, get_enrollments, get_platform_settings, @@ -28,7 +30,12 @@ from lms.djangoapps.learner_home.views import ( get_entitlements, ) from lms.djangoapps.learner_home.test_serializers import random_url -from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory as CatalogCourseRunFactory +from openedx.core.djangoapps.catalog.tests.factories import ( + CourseRunFactory as CatalogCourseRunFactory, +) +from openedx.core.djangoapps.content.course_overviews.tests.factories import ( + CourseOverviewFactory, +) from xmodule.modulestore.tests.django_utils import ( TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase, @@ -36,7 +43,7 @@ from xmodule.modulestore.tests.django_utils import ( from xmodule.modulestore.tests.factories import CourseFactory -ENTERPRISE_ENABLED = 'ENABLE_ENTERPRISE_INTEGRATION' +ENTERPRISE_ENABLED = "ENABLE_ENTERPRISE_INTEGRATION" class TestGetPlatformSettings(TestCase): @@ -173,7 +180,7 @@ class TestGetEntitlements(SharedModuleStoreTestCase): self, filtered_entitlements, course_entitlement_available_sessions, - unfulfilled_entitlement_pseudo_sessions + unfulfilled_entitlement_pseudo_sessions, ): """ Context manager utility for mocking get_filtered_course_entitlements. @@ -184,7 +191,10 @@ class TestGetEntitlements(SharedModuleStoreTestCase): course_entitlement_available_sessions, unfulfilled_entitlement_pseudo_sessions, ) - with patch('lms.djangoapps.learner_home.views.get_filtered_course_entitlements', return_value=return_value): + with patch( + "lms.djangoapps.learner_home.views.get_filtered_course_entitlements", + return_value=return_value, + ): yield def create_test_fulfilled_entitlement(self): @@ -213,7 +223,9 @@ class TestGetEntitlements(SharedModuleStoreTestCase): available_sessions = {} for entitlement in fulfilled_test_entitlements + unfulfilled_test_entitlements: - available_sessions[str(entitlement.uuid)] = CatalogCourseRunFactory.create_batch(3) + available_sessions[ + str(entitlement.uuid) + ] = CatalogCourseRunFactory.create_batch(3) pseudo_sessions = {} for entitlement in unfulfilled_test_entitlements: @@ -222,7 +234,7 @@ class TestGetEntitlements(SharedModuleStoreTestCase): with self.mock_get_filtered_course_entitlements( fulfilled_test_entitlements + unfulfilled_test_entitlements, available_sessions, - pseudo_sessions + pseudo_sessions, ): ( fulfilled_entitlements_by_course_key, @@ -231,7 +243,9 @@ class TestGetEntitlements(SharedModuleStoreTestCase): unfulfilled_entitlement_pseudo_sessions, ) = get_entitlements(self.user, None, None) - assert len(fulfilled_entitlements_by_course_key) == len(fulfilled_test_entitlements) + assert len(fulfilled_entitlements_by_course_key) == len( + fulfilled_test_entitlements + ) assert len(unfulfilled_entitlements) == len(unfulfilled_test_entitlements) assert set(unfulfilled_entitlements) == set(unfulfilled_test_entitlements) assert course_entitlement_available_sessions is available_sessions @@ -257,6 +271,43 @@ class TestGetEntitlements(SharedModuleStoreTestCase): assert not unfulfilled_entitlement_pseudo_sessions +class TestGetCourseOverviewsForPseudoSessions(SharedModuleStoreTestCase): + """Tests for get_course_overviews_for_pseudo_sessions""" + + def test_basic(self): + # Given several unfulfilled entitlements + unfulfilled_entitlement_uuids = [uuid4() for _ in range(3)] + pseudo_sessions = {} + for uuid in unfulfilled_entitlement_uuids: + pseudo_sessions[str(uuid)] = CatalogCourseRunFactory.create() + + # ... that have matching CourseOverviews + expected_course_overviews = {} + for pseudo_session in pseudo_sessions.values(): + course_key = CourseKey.from_string(pseudo_session["key"]) + mock_course = CourseFactory.create( + org=course_key.org, run=course_key.run, number=course_key.course + ) + mock_course_overview = CourseOverviewFactory.create(id=mock_course.id) + expected_course_overviews[course_key] = mock_course_overview + + # When I try to get course overviews, keyed by course key + course_overviews = get_course_overviews_for_pseudo_sessions(pseudo_sessions) + + # Then they map to the correct courses + self.assertDictEqual(course_overviews, expected_course_overviews) + + def test_no_pseudo_sessions(self): + # Given no pseudo sessions + pseudo_sessions = {} + + # When I query course overviews + course_overviews = get_course_overviews_for_pseudo_sessions(pseudo_sessions) + + # Then I should get an empty dict + self.assertDictEqual(course_overviews, {}) + + class TestGetEmailSettingsInfo(SharedModuleStoreTestCase): """Tests for get_email_settings_info""" diff --git a/lms/djangoapps/learner_home/views.py b/lms/djangoapps/learner_home/views.py index 441e4073c1..01cf948b5a 100644 --- a/lms/djangoapps/learner_home/views.py +++ b/lms/djangoapps/learner_home/views.py @@ -3,6 +3,7 @@ Views for the learner dashboard. """ from django.conf import settings from edx_django_utils import monitoring as monitoring_utils +from opaque_keys.edx.keys import CourseKey from rest_framework.response import Response from rest_framework.generics import RetrieveAPIView @@ -26,8 +27,11 @@ from lms.djangoapps.courseware.access_utils import ( check_course_open_for_learner, ) from lms.djangoapps.learner_home.serializers import LearnerDashboardSerializer +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers -from openedx.features.enterprise_support.api import enterprise_customer_from_session_or_learner_data +from openedx.features.enterprise_support.api import ( + enterprise_customer_from_session_or_learner_data, +) def get_platform_settings(): @@ -98,32 +102,48 @@ def get_enrollments(user, org_allow_list, org_block_list, course_limit=None): def get_entitlements(user, org_allow_list, org_block_list): - """Get entitlments for the user""" + """Get entitlements for the user""" ( filtered_entitlements, course_entitlement_available_sessions, - unfulfilled_entitlement_pseudo_sessions - ) = get_filtered_course_entitlements( - user, org_allow_list, org_block_list - ) + unfulfilled_entitlement_pseudo_sessions, + ) = get_filtered_course_entitlements(user, org_allow_list, org_block_list) fulfilled_entitlements_by_course_key = {} - unfulfulled_entitlements = [] + unfulfilled_entitlements = [] for course_entitlement in filtered_entitlements: if course_entitlement.enrollment_course_run: course_id = str(course_entitlement.enrollment_course_run.course.id) fulfilled_entitlements_by_course_key[course_id] = course_entitlement else: - unfulfulled_entitlements.append(course_entitlement) + unfulfilled_entitlements.append(course_entitlement) return ( fulfilled_entitlements_by_course_key, - unfulfulled_entitlements, + unfulfilled_entitlements, course_entitlement_available_sessions, unfulfilled_entitlement_pseudo_sessions, ) +def get_course_overviews_for_pseudo_sessions(unfulfilled_entitlement_pseudo_sessions): + """ + Get course overviews for entitlement pseudo sessions. This is required for + serializing course providers for entitlements. + + Returns: dict of course overviews, keyed by CourseKey + """ + course_ids = [] + + # Get course IDs from unfulfilled entitlement pseudo sessions + for pseudo_session in unfulfilled_entitlement_pseudo_sessions.values(): + course_id = pseudo_session.get("key") + if course_id: + course_ids.append(CourseKey.from_string(course_id)) + + return CourseOverview.get_from_ids(course_ids) + + def get_email_settings_info(user, course_enrollments): """ Given a user and enrollments, determine which courses allow bulk email (show_email_settings_for) @@ -231,13 +251,16 @@ class InitializeView(RetrieveAPIView): # pylint: disable=unused-argument # Get the org whitelist or the org blacklist for the current site site_org_whitelist, site_org_blacklist = get_org_black_and_whitelist_for_site() - # Get entitlements + # Get entitlements and course overviews for serializing ( fulfilled_entitlements_by_course_key, - unfulfulled_entitlements, + unfulfilled_entitlements, course_entitlement_available_sessions, - unfulfilled_entitlement_pseudo_sessions + unfulfilled_entitlement_pseudo_sessions, ) = get_entitlements(user, site_org_whitelist, site_org_blacklist) + pseudo_session_course_overviews = get_course_overviews_for_pseudo_sessions( + unfulfilled_entitlement_pseudo_sessions + ) # Get enrollments course_enrollments, course_mode_info = get_enrollments( @@ -268,7 +291,7 @@ class InitializeView(RetrieveAPIView): # pylint: disable=unused-argument "enterpriseDashboard": enterprise_customer, "platformSettings": get_platform_settings(), "enrollments": course_enrollments, - "unfulfilledEntitlements": unfulfulled_entitlements, + "unfulfilledEntitlements": unfulfilled_entitlements, "suggestedCourses": [], } @@ -283,6 +306,7 @@ class InitializeView(RetrieveAPIView): # pylint: disable=unused-argument "fulfilled_entitlements": fulfilled_entitlements_by_course_key, "course_entitlement_available_sessions": course_entitlement_available_sessions, "unfulfilled_entitlement_pseudo_sessions": unfulfilled_entitlement_pseudo_sessions, + "pseudo_session_course_overviews": pseudo_session_course_overviews, } response_data = LearnerDashboardSerializer(