diff --git a/common/djangoapps/student/tests/test_views.py b/common/djangoapps/student/tests/test_views.py index 96fe986e2a30abba466886b3d3e34e610bb7acb7..e3f85cb4e71f09574fa54f46d47e9a58d0b8769d 100644 --- a/common/djangoapps/student/tests/test_views.py +++ b/common/djangoapps/student/tests/test_views.py @@ -377,7 +377,8 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, 'key': 'course-v1:FAKE+FA1-MA1.X+3T2017', 'enrollment_end': str(self.TOMORROW), 'pacing_type': 'instructor_paced', - 'type': 'verified' + 'type': 'verified', + 'status': 'published' } ] mock_pseudo_session.return_value = { @@ -403,7 +404,8 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, 'key': 'course-v1:edX+toy+2012_Fall', 'enrollment_end': str(self.TOMORROW), 'pacing_type': 'instructor_paced', - 'type': 'verified' + 'type': 'verified', + 'status': 'published' } ] response = self.client.get(self.path) @@ -432,7 +434,8 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, 'key': 'course-v1:FAKE+FA1-MA1.X+3T2017', 'enrollment_end': str(self.TOMORROW), 'pacing_type': 'instructor_paced', - 'type': 'verified' + 'type': 'verified', + 'status': 'published' } ] response = self.client.get(self.path) @@ -464,7 +467,8 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, 'key': str(mocked_course_overview.id), 'enrollment_end': str(mocked_course_overview.enrollment_end), 'pacing_type': 'self_paced', - 'type': 'verified' + 'type': 'verified', + 'status': 'published' } ] CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment) @@ -482,7 +486,8 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, 'key': str(mocked_course_overview.id), 'enrollment_end': str(mocked_course_overview.enrollment_end), 'pacing_type': 'self_paced', - 'type': 'verified' + 'type': 'verified', + 'status': 'published' } ] # response = self.client.get(self.path) @@ -499,7 +504,8 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, 'key': str(mocked_course_overview.id), 'enrollment_end': None, 'pacing_type': 'self_paced', - 'type': 'verified' + 'type': 'verified', + 'status': 'published' } ] # response = self.client.get(self.path) @@ -529,7 +535,8 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, 'key': str(mocked_course_overview.id), 'enrollment_end': str(mocked_course_overview.enrollment_end), 'pacing_type': 'self_paced', - 'type': 'verified' + 'type': 'verified', + 'status': 'published' } ] entitlement = CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment) @@ -565,7 +572,8 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, 'key': str(mocked_course_overview.id), 'enrollment_end': str(mocked_course_overview.enrollment_end), 'pacing_type': 'self_paced', - 'type': 'verified' + 'type': 'verified', + 'status': 'published' } ] entitlement = CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment, created=self.THREE_YEARS_AGO) diff --git a/openedx/core/constants.py b/openedx/core/constants.py index 022b3511f54fc37efc399dcbd43162f91114cc2c..ad16e9e595296807985e5545844b9b8bc045ca77 100644 --- a/openedx/core/constants.py +++ b/openedx/core/constants.py @@ -10,3 +10,5 @@ Constants that are relevant to all of Open edX COURSE_KEY_PATTERN = r'(?P<course_key_string>[^/+]+(/|\+)[^/+]+(/|\+)[^/?]+)' COURSE_ID_PATTERN = COURSE_KEY_PATTERN.replace('course_key_string', 'course_id') COURSE_KEY_REGEX = COURSE_KEY_PATTERN.replace('P<course_key_string>', ':') +COURSE_PUBLISHED = 'published' +COURSE_UNPUBLISHED = 'unpublished' diff --git a/openedx/core/djangoapps/catalog/tests/test_utils.py b/openedx/core/djangoapps/catalog/tests/test_utils.py index d7bb2b57322fb6a579e17389674afeb64f89ba08..60ebb2a29e6cbb782d2e446eeb5a31bc4de64e77 100644 --- a/openedx/core/djangoapps/catalog/tests/test_utils.py +++ b/openedx/core/djangoapps/catalog/tests/test_utils.py @@ -1,6 +1,7 @@ """Tests covering utilities for integrating with the catalog service.""" # pylint: disable=missing-docstring import copy +from datetime import timedelta import ddt import mock @@ -8,8 +9,13 @@ from django.contrib.auth import get_user_model from django.core.cache import cache from django.test import TestCase, override_settings from django.test.client import RequestFactory -from student.tests.factories import UserFactory +from django.utils.timezone import now +from opaque_keys.edx.keys import CourseKey +from course_modes.helpers import CourseMode +from course_modes.tests.factories import CourseModeFactory +from entitlements.tests.factories import CourseEntitlementFactory +from openedx.core.constants import COURSE_UNPUBLISHED from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL, SITE_PROGRAM_UUIDS_CACHE_KEY_TPL from openedx.core.djangoapps.catalog.models import CatalogIntegration from openedx.core.djangoapps.catalog.tests.factories import ( @@ -27,10 +33,12 @@ from openedx.core.djangoapps.catalog.utils import ( get_localized_price_text, get_program_types, get_programs, - get_programs_with_type + get_visible_sessions_for_entitlement ) +from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms +from student.tests.factories import CourseEnrollmentFactory, UserFactory UTILS_MODULE = 'openedx.core.djangoapps.catalog.utils' User = get_user_model() # pylint: disable=invalid-name @@ -316,6 +324,60 @@ class TestGetCourseRuns(CatalogIntegrationMixin, TestCase): self.assertEqual(data, catalog_course_runs) +@skip_unless_lms +@mock.patch(UTILS_MODULE + '.get_edx_api_data') +class TestSessionEntitlement(CatalogIntegrationMixin, TestCase): + """ + Test Covering data related Entitlements. + """ + def setUp(self): + super(TestSessionEntitlement, self).setUp() + + self.catalog_integration = self.create_catalog_integration(cache_ttl=1) + self.user = UserFactory(username=self.catalog_integration.service_username) + self.tomorrow = now() + timedelta(days=1) + + def test_get_visible_sessions_for_entitlement(self, mock_get_edx_api_data): + """ + Test retrieval of visible session entitlements. + """ + catalog_course_runs = CourseRunFactory.create() + catalog_course = CourseFactory(course_runs=[catalog_course_runs]) + mock_get_edx_api_data.return_value = catalog_course + course_key = CourseKey.from_string(catalog_course_runs.get('key')) + course_overview = CourseOverviewFactory.create(id=course_key, start=self.tomorrow) + CourseModeFactory.create(mode_slug=CourseMode.VERIFIED, min_price=100, course_id=course_overview.id) + course_enrollment = CourseEnrollmentFactory( + user=self.user, course_id=unicode(course_overview.id), mode=CourseMode.VERIFIED + ) + entitlement = CourseEntitlementFactory( + user=self.user, enrollment_course_run=course_enrollment, mode=CourseMode.VERIFIED + ) + + session_entitlements = get_visible_sessions_for_entitlement(entitlement) + self.assertEqual(session_entitlements, [catalog_course_runs]) + + def test_unpublished_sessions_for_entitlement(self, mock_get_edx_api_data): + """ + Test unpublished course runs are not part of visible session entitlements. + """ + catalog_course_runs = CourseRunFactory.create(status=COURSE_UNPUBLISHED) + catalog_course = CourseFactory(course_runs=[catalog_course_runs]) + mock_get_edx_api_data.return_value = catalog_course + course_key = CourseKey.from_string(catalog_course_runs.get('key')) + course_overview = CourseOverviewFactory.create(id=course_key, start=self.tomorrow) + CourseModeFactory.create(mode_slug=CourseMode.VERIFIED, min_price=100, course_id=course_overview.id) + course_enrollment = CourseEnrollmentFactory( + user=self.user, course_id=unicode(course_overview.id), mode=CourseMode.VERIFIED + ) + entitlement = CourseEntitlementFactory( + user=self.user, enrollment_course_run=course_enrollment, mode=CourseMode.VERIFIED + ) + + session_entitlements = get_visible_sessions_for_entitlement(entitlement) + self.assertEqual(session_entitlements, []) + + @skip_unless_lms @mock.patch(UTILS_MODULE + '.get_edx_api_data') class TestGetCourseRunDetails(CatalogIntegrationMixin, TestCase): diff --git a/openedx/core/djangoapps/catalog/utils.py b/openedx/core/djangoapps/catalog/utils.py index 68fee4459a888ee381269bd6da5fe2722536e2b9..51cf34ce11e99126b82fffecb583152965aa18ba 100644 --- a/openedx/core/djangoapps/catalog/utils.py +++ b/openedx/core/djangoapps/catalog/utils.py @@ -13,6 +13,7 @@ from opaque_keys.edx.keys import CourseKey from pytz import UTC from entitlements.utils import is_course_run_entitlement_fulfillable +from openedx.core.constants import COURSE_PUBLISHED from openedx.core.djangoapps.catalog.cache import (PROGRAM_CACHE_KEY_TPL, SITE_PROGRAM_UUIDS_CACHE_KEY_TPL) from openedx.core.djangoapps.catalog.models import CatalogIntegration @@ -379,13 +380,21 @@ def get_visible_sessions_for_entitlement(entitlement): def get_fulfillable_course_runs_for_entitlement(entitlement, course_runs): """ - Takes a list of course runs and returns only the course runs, sorted by start date, that: + Looks through the list of course runs and returns the course runs that can + be applied to the entitlement. - These are the only sessions that can be selected for an entitlement. + Args: + entitlement (CourseEntitlement): The CourseEntitlement to which a + course run is to be applied. + course_runs (list): List of course run that we would like to apply + to the entitlement. + + Return: + list: A list of sessions that a user can apply to the provided entitlement. """ enrollable_sessions = [] - # Only show published course runs that can still be enrolled and upgraded + # Only retrieve list of published course runs that can still be enrolled and upgraded search_time = datetime.datetime.now(UTC) for course_run in course_runs: course_id = CourseKey.from_string(course_run.get('key')) @@ -394,7 +403,8 @@ def get_fulfillable_course_runs_for_entitlement(entitlement, course_runs): course_id=course_id ) is_enrolled_in_mode = is_active and (user_enrollment_mode == entitlement.mode) - if is_course_run_entitlement_fulfillable(course_id, entitlement, search_time): + if (course_run.get('status') == COURSE_PUBLISHED and + is_course_run_entitlement_fulfillable(course_id, entitlement, search_time)): if (is_enrolled_in_mode and entitlement.enrollment_course_run and course_id == entitlement.enrollment_course_run.course_id):