Commit bde0f7b2 by wajeeha-khalid Committed by GitHub

Merge pull request #13235 from edx/jia/MA-2684

MA-2684: return correct marketing url for course about
parents 71693c3a 11356965
...@@ -76,7 +76,7 @@ from student.auth import has_course_author_access, has_studio_write_access, has_ ...@@ -76,7 +76,7 @@ from student.auth import has_course_author_access, has_studio_write_access, has_
from student.roles import ( from student.roles import (
CourseInstructorRole, CourseStaffRole, CourseCreatorRole, GlobalStaff, UserBasedRole CourseInstructorRole, CourseStaffRole, CourseCreatorRole, GlobalStaff, UserBasedRole
) )
from util.course import get_lms_link_for_about_page from util.course import get_link_for_about_page
from util.date_utils import get_default_time_display from util.date_utils import get_default_time_display
from util.json_request import JsonResponse, JsonResponseBadRequest, expect_json from util.json_request import JsonResponse, JsonResponseBadRequest, expect_json
from util.milestones_helpers import ( from util.milestones_helpers import (
...@@ -1000,7 +1000,7 @@ def settings_handler(request, course_key_string): ...@@ -1000,7 +1000,7 @@ def settings_handler(request, course_key_string):
settings_context = { settings_context = {
'context_course': course_module, 'context_course': course_module,
'course_locator': course_key, 'course_locator': course_key,
'lms_link_for_about_page': get_lms_link_for_about_page(course_key), 'lms_link_for_about_page': get_link_for_about_page(course_key, request.user),
'course_image_url': course_image_url(course_module, 'course_image'), 'course_image_url': course_image_url(course_module, 'course_image'),
'banner_image_url': course_image_url(course_module, 'banner_image'), 'banner_image_url': course_image_url(course_module, 'banner_image'),
'video_thumbnail_image_url': course_image_url(course_module, 'video_thumbnail_image'), 'video_thumbnail_image_url': course_image_url(course_module, 'video_thumbnail_image'),
......
...@@ -2,27 +2,30 @@ ...@@ -2,27 +2,30 @@
Utility methods related to course Utility methods related to course
""" """
import logging import logging
from django.conf import settings
from django.conf import settings
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.catalog.utils import get_run_marketing_url
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def get_lms_link_for_about_page(course_key): def get_link_for_about_page(course_key, user, catalog_course_run=None):
""" """
Returns the url to the course about page. Returns the url to the course about page.
""" """
assert isinstance(course_key, CourseKey) assert isinstance(course_key, CourseKey)
if settings.FEATURES.get('ENABLE_MKTG_SITE'): if settings.FEATURES.get('ENABLE_MKTG_SITE'):
# Root will be "https://www.edx.org". The complete URL will still not be exactly correct, if catalog_course_run:
# but redirects exist from www.edx.org to get to the Drupal course about page URL. marketing_url = catalog_course_run.get('marketing_url')
about_base = settings.MKTG_URLS['ROOT']
else: else:
about_base = settings.LMS_ROOT_URL marketing_url = get_run_marketing_url(course_key, user)
if marketing_url:
return marketing_url
return u"{about_base_url}/courses/{course_key}/about".format( return u"{about_base_url}/courses/{course_key}/about".format(
about_base_url=about_base, about_base_url=settings.LMS_ROOT_URL,
course_key=course_key.to_deprecated_string() course_key=unicode(course_key)
) )
""" """
Tests for course utils. Tests for course utils.
""" """
from django.core.cache import cache
from django.test import TestCase, override_settings import httpretty
import mock import mock
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.keys import CourseKey
from util.course import get_lms_link_for_about_page
from openedx.core.djangoapps.catalog.tests import factories
from openedx.core.djangoapps.catalog.utils import CatalogCacheUtility
from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from student.tests.factories import UserFactory
from util.course import get_link_for_about_page
@httpretty.activate
class CourseAboutLinkTestCase(CatalogIntegrationMixin, CacheIsolationTestCase):
"""
Tests for Course About link.
"""
ENABLED_CACHES = ['default']
def setUp(self):
super(CourseAboutLinkTestCase, self).setUp()
self.user = UserFactory.create(password="password")
class LmsLinksTestCase(TestCase): self.course_key_string = "foo/bar/baz"
""" Tests for LMS links. """ self.course_key = CourseKey.from_string("foo/bar/baz")
self.course_run = factories.CourseRun(key=self.course_key_string)
self.lms_course_about_url = "http://localhost:8000/courses/foo/bar/baz/about"
def test_about_page(self): self.catalog_integration = self.create_catalog_integration(
""" Get URL for about page, no marketing site """ internal_api_url="http://catalog.example.com:443/api/v1",
cache_ttl=1
)
self.course_cache_key = "{}{}".format(CatalogCacheUtility.CACHE_KEY_PREFIX, self.course_key_string)
def test_about_page_lms(self):
"""
Get URL for about page, no marketing site.
"""
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False}): with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False}):
self.assertEquals(self.get_about_page_link(), "http://localhost:8000/courses/mitX/101/test/about") self.assertEquals(
get_link_for_about_page(self.course_key, self.user), self.lms_course_about_url
)
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
self.register_catalog_course_run_response(
[self.course_key_string], [{"key": self.course_key_string, "marketing_url": None}]
)
self.assertEquals(get_link_for_about_page(self.course_key, self.user), self.lms_course_about_url)
@override_settings(MKTG_URLS={'ROOT': 'https://dummy-root'}) @mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True})
def test_about_page_marketing_site(self): def test_about_page_marketing_site(self):
""" Get URL for about page, marketing root present. """ """
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}): Get URL for about page, marketing site enabled.
self.assertEquals(self.get_about_page_link(), "https://dummy-root/courses/mitX/101/test/about") """
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False}): self.register_catalog_course_run_response([self.course_key_string], [self.course_run])
self.assertEquals(self.get_about_page_link(), "http://localhost:8000/courses/mitX/101/test/about") self.assertEquals(get_link_for_about_page(self.course_key, self.user), self.course_run["marketing_url"])
cached_data = cache.get_many([self.course_cache_key])
self.assertIn(self.course_cache_key, cached_data.keys())
@override_settings(MKTG_URLS={'ROOT': 'https://www.dummyhttps://x'}) with mock.patch('openedx.core.djangoapps.catalog.utils.get_edx_api_data') as mock_method:
def test_about_page_marketing_site_https__edge(self): self.assertEquals(get_link_for_about_page(self.course_key, self.user), self.course_run["marketing_url"])
""" Get URL for about page """ self.assertEqual(0, mock_method.call_count)
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True}):
self.assertEquals(self.get_about_page_link(), "https://www.dummyhttps://x/courses/mitX/101/test/about")
def get_about_page_link(self): @mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True})
""" create mock course and return the about page link.""" def test_about_page_marketing_url_cached(self):
course_key = SlashSeparatedCourseKey('mitX', '101', 'test') self.assertEquals(
return get_lms_link_for_about_page(course_key) get_link_for_about_page(self.course_key, self.user, self.course_run),
self.course_run["marketing_url"]
)
...@@ -17,6 +17,8 @@ import httpretty ...@@ -17,6 +17,8 @@ import httpretty
import mock import mock
from provider.constants import CONFIDENTIAL from provider.constants import CONFIDENTIAL
from openedx.core.djangoapps.catalog.tests import factories as catalog_factories
from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.credentials.tests import factories as credentials_factories from openedx.core.djangoapps.credentials.tests import factories as credentials_factories
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin
...@@ -265,9 +267,10 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar ...@@ -265,9 +267,10 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar
@httpretty.activate @httpretty.activate
@override_settings(MKTG_URLS={'ROOT': 'https://www.example.com'}) @override_settings(MKTG_URLS={'ROOT': 'https://www.example.com'})
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@mock.patch(UTILS_MODULE + '.get_run_marketing_url', mock.Mock(return_value=MARKETING_URL)) class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase, CatalogIntegrationMixin):
class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): """
"""Unit tests for the program details page.""" Unit tests for the program details page.
"""
program_id = 123 program_id = 123
password = 'test' password = 'test'
url = reverse('program_details_view', args=[program_id]) url = reverse('program_details_view', args=[program_id])
...@@ -279,6 +282,7 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): ...@@ -279,6 +282,7 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
course = CourseFactory() course = CourseFactory()
cls.course_id_string = unicode(course.id) # pylint: disable=no-member
organization = programs_factories.Organization() organization = programs_factories.Organization()
run_mode = programs_factories.RunMode(course_key=unicode(course.id)) # pylint: disable=no-member run_mode = programs_factories.RunMode(course_key=unicode(course.id)) # pylint: disable=no-member
course_code = programs_factories.CourseCode(run_modes=[run_mode]) course_code = programs_factories.CourseCode(run_modes=[run_mode])
...@@ -287,6 +291,7 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): ...@@ -287,6 +291,7 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
organizations=[organization], organizations=[organization],
course_codes=[course_code] course_codes=[course_code]
) )
cls.course_run = catalog_factories.CourseRun(key=cls.course_id_string)
def setUp(self): def setUp(self):
super(TestProgramDetails, self).setUp() super(TestProgramDetails, self).setUp()
...@@ -295,7 +300,9 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): ...@@ -295,7 +300,9 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
self.client.login(username=self.user.username, password=self.password) self.client.login(username=self.user.username, password=self.password)
def mock_programs_api(self, data, status=200): def mock_programs_api(self, data, status=200):
"""Helper for mocking out Programs API URLs.""" """
Helper for mocking out Programs API URLs.
"""
self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Programs API calls.') self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Programs API calls.')
url = '{api_root}/programs/{id}/'.format( url = '{api_root}/programs/{id}/'.format(
...@@ -314,7 +321,9 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): ...@@ -314,7 +321,9 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
) )
def assert_program_data_present(self, response): def assert_program_data_present(self, response):
"""Verify that program data is present.""" """
Verify that program data is present.
"""
self.assertContains(response, 'programData') self.assertContains(response, 'programData')
self.assertContains(response, 'urls') self.assertContains(response, 'urls')
self.assertContains(response, 'program_listing_url') self.assertContains(response, 'program_listing_url')
...@@ -322,7 +331,9 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): ...@@ -322,7 +331,9 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
self.assert_programs_tab_present(response) self.assert_programs_tab_present(response)
def assert_programs_tab_present(self, response): def assert_programs_tab_present(self, response):
"""Verify that the programs tab is present in the nav.""" """
Verify that the programs tab is present in the nav.
"""
soup = BeautifulSoup(response.content, 'html.parser') soup = BeautifulSoup(response.content, 'html.parser')
self.assertTrue( self.assertTrue(
any(soup.find_all('a', class_='tab-nav-link', href=reverse('program_listing_view'))) any(soup.find_all('a', class_='tab-nav-link', href=reverse('program_listing_view')))
...@@ -332,6 +343,9 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): ...@@ -332,6 +343,9 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
""" """
Verify that login is required to access the page. Verify that login is required to access the page.
""" """
with mock.patch('openedx.core.djangoapps.catalog.utils.get_course_runs', return_value={
self.course_id_string: self.course_run,
}):
self.create_programs_config() self.create_programs_config()
self.mock_programs_api(self.data) self.mock_programs_api(self.data)
...@@ -358,7 +372,9 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): ...@@ -358,7 +372,9 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_404_if_no_data(self): def test_404_if_no_data(self):
"""Verify that the page 404s if no program data is found.""" """
Verify that the page 404s if no program data is found.
"""
self.create_programs_config() self.create_programs_config()
self.mock_programs_api(self.data, status=404) self.mock_programs_api(self.data, status=404)
...@@ -372,7 +388,9 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): ...@@ -372,7 +388,9 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_page_routing(self): def test_page_routing(self):
"""Verify that the page can be hit with or without a program name in the URL.""" """
Verify that the page can be hit with or without a program name in the URL.
"""
self.create_programs_config() self.create_programs_config()
self.mock_programs_api(self.data) self.mock_programs_api(self.data)
......
""" """
Serializer for user API Serializer for user API
""" """
from opaque_keys.edx.keys import CourseKey
from rest_framework import serializers from rest_framework import serializers
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from courseware.access import has_access from courseware.access import has_access
from student.models import CourseEnrollment, User
from certificates.api import certificate_downloadable_status from certificates.api import certificate_downloadable_status
from util.course import get_lms_link_for_about_page from student.models import CourseEnrollment, User
from util.course import get_link_for_about_page
class CourseOverviewField(serializers.RelatedField): class CourseOverviewField(serializers.RelatedField):
...@@ -52,7 +50,9 @@ class CourseOverviewField(serializers.RelatedField): ...@@ -52,7 +50,9 @@ class CourseOverviewField(serializers.RelatedField):
} }
}, },
'course_image': course_overview.course_image_url, 'course_image': course_overview.course_image_url,
'course_about': get_lms_link_for_about_page(CourseKey.from_string(course_id)), 'course_about': get_link_for_about_page(
course_overview.id, request.user, self.context.get('catalog_course_run')
),
'course_updates': reverse( 'course_updates': reverse(
'course-updates-list', 'course-updates-list',
kwargs={'course_id': course_id}, kwargs={'course_id': course_id},
...@@ -85,7 +85,9 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer): ...@@ -85,7 +85,9 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
certificate = serializers.SerializerMethodField() certificate = serializers.SerializerMethodField()
def get_certificate(self, model): def get_certificate(self, model):
"""Returns the information about the user's certificate in the course.""" """
Returns the information about the user's certificate in the course.
"""
certificate_info = certificate_downloadable_status(model.user, model.course_id) certificate_info = certificate_downloadable_status(model.user, model.course_id)
if certificate_info['is_downloadable']: if certificate_info['is_downloadable']:
return { return {
......
...@@ -5,7 +5,9 @@ Tests for users API ...@@ -5,7 +5,9 @@ Tests for users API
import datetime import datetime
import ddt import ddt
import httpretty
from mock import patch from mock import patch
import mock
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
import pytz import pytz
from django.conf import settings from django.conf import settings
...@@ -26,6 +28,9 @@ from courseware.access_response import ( ...@@ -26,6 +28,9 @@ from courseware.access_response import (
) )
from course_modes.models import CourseMode from course_modes.models import CourseMode
from lms.djangoapps.grades.tests.utils import mock_passing_grade from lms.djangoapps.grades.tests.utils import mock_passing_grade
from openedx.core.djangoapps.catalog.tests import factories
from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin
from openedx.core.djangoapps.catalog.utils import get_course_runs
from openedx.core.lib.courses import course_image_url from openedx.core.lib.courses import course_image_url
from student.models import CourseEnrollment from student.models import CourseEnrollment
from util.milestones_helpers import set_prerequisite_courses from util.milestones_helpers import set_prerequisite_courses
...@@ -463,7 +468,8 @@ class TestCourseStatusPATCH(CourseStatusAPITestCase, MobileAuthUserTestMixin, ...@@ -463,7 +468,8 @@ class TestCourseStatusPATCH(CourseStatusAPITestCase, MobileAuthUserTestMixin,
@attr(shard=2) @attr(shard=2)
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True}) @patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True})
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'}) @override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
class TestCourseEnrollmentSerializer(MobileAPITestCase, MilestonesTestCaseMixin): @httpretty.activate
class TestCourseEnrollmentSerializer(MobileAPITestCase, MilestonesTestCaseMixin, CatalogIntegrationMixin):
""" """
Test the course enrollment serializer Test the course enrollment serializer
""" """
...@@ -472,6 +478,13 @@ class TestCourseEnrollmentSerializer(MobileAPITestCase, MilestonesTestCaseMixin) ...@@ -472,6 +478,13 @@ class TestCourseEnrollmentSerializer(MobileAPITestCase, MilestonesTestCaseMixin)
self.login_and_enroll() self.login_and_enroll()
self.request = RequestFactory().get('/') self.request = RequestFactory().get('/')
self.request.user = self.user self.request.user = self.user
self.catalog_integration = self.create_catalog_integration(
internal_api_url="http://catalog.example.com:443/api/v1",
cache_ttl=1
)
self.course_id_string = unicode(self.course.id)
self.course_run = factories.CourseRun(key=self.course_id_string)
self.course_keys = [self.course_id_string]
def test_success(self): def test_success(self):
serialized = CourseEnrollmentSerializer( serialized = CourseEnrollmentSerializer(
...@@ -493,3 +506,41 @@ class TestCourseEnrollmentSerializer(MobileAPITestCase, MilestonesTestCaseMixin) ...@@ -493,3 +506,41 @@ class TestCourseEnrollmentSerializer(MobileAPITestCase, MilestonesTestCaseMixin)
).data ).data
self.assertEqual(serialized['course']['number'], self.course.display_coursenumber) self.assertEqual(serialized['course']['number'], self.course.display_coursenumber)
self.assertEqual(serialized['course']['org'], self.course.display_organization) self.assertEqual(serialized['course']['org'], self.course.display_organization)
@mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': True})
def test_course_about_marketing_url(self):
self.register_catalog_course_run_response(self.course_keys, [self.course_run])
catalog_course_runs_against_course_keys = get_course_runs(self.course_keys, self.request.user)
enrollment = CourseEnrollment.enrollments_for_user(self.user)[0]
serialized = CourseEnrollmentSerializer(
enrollment,
context={
'request': self.request,
"catalog_course_run": (
catalog_course_runs_against_course_keys[unicode(enrollment.course_id)]
if unicode(enrollment.course_id) in catalog_course_runs_against_course_keys else None
)
},
).data
self.assertEqual(
serialized['course']['course_about'], self.course_run["marketing_url"]
)
@mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_MKTG_SITE': False})
def test_course_about_lms_url(self):
self.register_catalog_course_run_response(self.course_keys, [self.course_run])
catalog_course_runs_against_course_keys = get_course_runs(self.course_keys, self.request.user)
enrollment = CourseEnrollment.enrollments_for_user(self.user)[0]
serialized = CourseEnrollmentSerializer(
enrollment,
context={
'request': self.request,
"catalog_course_run": (
catalog_course_runs_against_course_keys[unicode(enrollment.course_id)]
if unicode(enrollment.course_id) in catalog_course_runs_against_course_keys else None
)
},
).data
self.assertEqual(
serialized['course']['course_about'], "http://localhost:8000/courses/{}/about".format(self.course_id_string)
)
...@@ -11,12 +11,14 @@ from rest_framework.response import Response ...@@ -11,12 +11,14 @@ from rest_framework.response import Response
from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.keys import UsageKey
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from rest_framework.views import APIView
from courseware.access import is_mobile_available_for_user from courseware.access import is_mobile_available_for_user
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from courseware.module_render import get_module_for_descriptor from courseware.module_render import get_module_for_descriptor
from courseware.views.index import save_positions_recursively_up from courseware.views.index import save_positions_recursively_up
from courseware.views.views import get_current_child from courseware.views.views import get_current_child
from openedx.core.djangoapps.catalog.utils import get_course_runs
from student.models import CourseEnrollment, User from student.models import CourseEnrollment, User
from xblock.fields import Scope from xblock.fields import Scope
...@@ -204,7 +206,7 @@ class UserCourseStatus(views.APIView): ...@@ -204,7 +206,7 @@ class UserCourseStatus(views.APIView):
@mobile_view(is_user=True) @mobile_view(is_user=True)
class UserCourseEnrollmentsList(generics.ListAPIView): class UserCourseEnrollmentsList(APIView):
""" """
**Use Case** **Use Case**
...@@ -258,17 +260,6 @@ class UserCourseEnrollmentsList(generics.ListAPIView): ...@@ -258,17 +260,6 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
certified). certified).
* url: URL to the downloadable version of the certificate, if exists. * url: URL to the downloadable version of the certificate, if exists.
""" """
queryset = CourseEnrollment.objects.all()
serializer_class = CourseEnrollmentSerializer
lookup_field = 'username'
# In Django Rest Framework v3, there is a default pagination
# class that transmutes the response data into a dictionary
# with pagination information. The original response data (a list)
# is stored in a "results" value of the dictionary.
# For backwards compatibility with the existing API, we disable
# the default behavior by setting the pagination_class to None.
pagination_class = None
def is_org(self, check_org, course_org): def is_org(self, check_org, course_org):
""" """
...@@ -276,17 +267,34 @@ class UserCourseEnrollmentsList(generics.ListAPIView): ...@@ -276,17 +267,34 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
""" """
return check_org is None or (check_org.lower() == course_org.lower()) return check_org is None or (check_org.lower() == course_org.lower())
def get_queryset(self): def get(self, request, username):
enrollments = self.queryset.filter( """
user__username=self.kwargs['username'], Returns a list of courses enrolled by user.
"""
queryset = CourseEnrollment.objects.all()
course_ids = set(queryset.values_list('course_id', flat=True))
catalog_course_runs_against_course_keys = get_course_runs(course_ids, request.user)
enrollments = queryset.filter(
user__username=username,
is_active=True is_active=True
).order_by('created').reverse() ).order_by('created').reverse()
org = self.request.query_params.get('org', None) org = request.query_params.get('org', None)
return [
enrollment for enrollment in enrollments return Response([
CourseEnrollmentSerializer(
enrollment,
context={
"request": request,
"catalog_course_run": (
catalog_course_runs_against_course_keys[unicode(enrollment.course_id)]
if unicode(enrollment.course_id) in catalog_course_runs_against_course_keys else None
)
}
).data for enrollment in enrollments
if enrollment.course_overview and self.is_org(org, enrollment.course_overview.org) and if enrollment.course_overview and self.is_org(org, enrollment.course_overview.org) and
is_mobile_available_for_user(self.request.user, enrollment.course_overview) is_mobile_available_for_user(request.user, enrollment.course_overview)
] ])
@api_view(["GET"]) @api_view(["GET"])
......
"""Mixins to help test catalog integration.""" """Mixins to help test catalog integration."""
import json
import urllib
import httpretty
from openedx.core.djangoapps.catalog.models import CatalogIntegration from openedx.core.djangoapps.catalog.models import CatalogIntegration
class CatalogIntegrationMixin(object): class CatalogIntegrationMixin(object):
"""Utility for working with the catalog service during testing.""" """
Utility for working with the catalog service during testing.
"""
DEFAULTS = { DEFAULTS = {
'enabled': True, 'enabled': True,
...@@ -12,8 +19,27 @@ class CatalogIntegrationMixin(object): ...@@ -12,8 +19,27 @@ class CatalogIntegrationMixin(object):
} }
def create_catalog_integration(self, **kwargs): def create_catalog_integration(self, **kwargs):
"""Creates a new CatalogIntegration with DEFAULTS, updated with any provided overrides.""" """
Creates a new CatalogIntegration with DEFAULTS, updated with any provided overrides.
"""
fields = dict(self.DEFAULTS, **kwargs) fields = dict(self.DEFAULTS, **kwargs)
CatalogIntegration(**fields).save() CatalogIntegration(**fields).save()
return CatalogIntegration.current() return CatalogIntegration.current()
def register_catalog_course_run_response(self, course_keys, catalog_course_run_data):
"""
Register a mock response for GET on the catalog course run endpoint.
"""
httpretty.register_uri(
httpretty.GET,
"http://catalog.example.com:443/api/v1/course_runs/?keys={}&exclude_utm=1".format(
urllib.quote_plus(",".join(course_keys))
),
body=json.dumps({
"results": catalog_course_run_data,
"next": ""
}),
content_type='application/json',
status=200
)
"""Tests covering utilities for integrating with the catalog service.""" """
Tests covering utilities for integrating with the catalog service.
"""
import uuid import uuid
import ddt from django.core.cache import cache
from django.test import TestCase from django.test import TestCase
import httpretty
import mock import mock
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.catalog import utils from openedx.core.djangoapps.catalog import utils
from openedx.core.djangoapps.catalog.models import CatalogIntegration from openedx.core.djangoapps.catalog.models import CatalogIntegration
from openedx.core.djangoapps.catalog.tests import factories, mixins from openedx.core.djangoapps.catalog.tests import factories, mixins
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
...@@ -19,7 +23,9 @@ UTILS_MODULE = 'openedx.core.djangoapps.catalog.utils' ...@@ -19,7 +23,9 @@ UTILS_MODULE = 'openedx.core.djangoapps.catalog.utils'
# ConfigurationModels use the cache. Make every cache get a miss. # ConfigurationModels use the cache. Make every cache get a miss.
@mock.patch('config_models.models.cache.get', return_value=None) @mock.patch('config_models.models.cache.get', return_value=None)
class TestGetPrograms(mixins.CatalogIntegrationMixin, TestCase): class TestGetPrograms(mixins.CatalogIntegrationMixin, TestCase):
"""Tests covering retrieval of programs from the catalog service.""" """
Tests covering retrieval of programs from the catalog service.
"""
def setUp(self): def setUp(self):
super(TestGetPrograms, self).setUp() super(TestGetPrograms, self).setUp()
...@@ -29,7 +35,9 @@ class TestGetPrograms(mixins.CatalogIntegrationMixin, TestCase): ...@@ -29,7 +35,9 @@ class TestGetPrograms(mixins.CatalogIntegrationMixin, TestCase):
self.catalog_integration = self.create_catalog_integration(cache_ttl=1) self.catalog_integration = self.create_catalog_integration(cache_ttl=1)
def assert_contract(self, call_args, program_uuid=None, type=None): # pylint: disable=redefined-builtin def assert_contract(self, call_args, program_uuid=None, type=None): # pylint: disable=redefined-builtin
"""Verify that API data retrieval utility is used correctly.""" """
Verify that API data retrieval utility is used correctly.
"""
args, kwargs = call_args args, kwargs = call_args
for arg in (self.catalog_integration, self.user, 'programs'): for arg in (self.catalog_integration, self.user, 'programs'):
...@@ -108,7 +116,9 @@ class TestGetPrograms(mixins.CatalogIntegrationMixin, TestCase): ...@@ -108,7 +116,9 @@ class TestGetPrograms(mixins.CatalogIntegrationMixin, TestCase):
class TestMungeCatalogProgram(TestCase): class TestMungeCatalogProgram(TestCase):
"""Tests covering querystring stripping.""" """
Tests covering querystring stripping.
"""
catalog_program = factories.Program() catalog_program = factories.Program()
def test_munge_catalog_program(self): def test_munge_catalog_program(self):
...@@ -153,90 +163,173 @@ class TestMungeCatalogProgram(TestCase): ...@@ -153,90 +163,173 @@ class TestMungeCatalogProgram(TestCase):
self.assertEqual(munged, expected) self.assertEqual(munged, expected)
@mock.patch(UTILS_MODULE + '.get_edx_api_data') @httpretty.activate
@mock.patch('config_models.models.cache.get', return_value=None) class TestGetCourseRun(mixins.CatalogIntegrationMixin, CacheIsolationTestCase):
class TestGetCourseRun(mixins.CatalogIntegrationMixin, TestCase): """
"""Tests covering retrieval of course runs from the catalog service.""" Tests covering retrieval of course runs from the catalog service.
"""
ENABLED_CACHES = ['default']
def setUp(self): def setUp(self):
super(TestGetCourseRun, self).setUp() super(TestGetCourseRun, self).setUp()
self.user = UserFactory() self.user = UserFactory()
self.course_key = CourseKey.from_string('foo/bar/baz') self.catalog_integration = self.create_catalog_integration(
self.catalog_integration = self.create_catalog_integration() internal_api_url="http://catalog.example.com:443/api/v1",
cache_ttl=1,
def assert_contract(self, call_args): )
"""Verify that API data retrieval utility is used correctly.""" self.course_runs = [factories.CourseRun() for __ in range(4)]
args, kwargs = call_args self.course_key_1 = CourseKey.from_string(self.course_runs[0]["key"])
self.course_key_2 = CourseKey.from_string(self.course_runs[1]["key"])
for arg in (self.catalog_integration, self.user, 'course_runs'): self.course_key_3 = CourseKey.from_string(self.course_runs[2]["key"])
self.assertIn(arg, args) self.course_key_4 = CourseKey.from_string(self.course_runs[3]["key"])
self.assertEqual(kwargs['resource_id'], unicode(self.course_key)) def test_config_missing(self):
self.assertEqual(kwargs['api']._store['base_url'], self.catalog_integration.internal_api_url) # pylint: disable=protected-access """
Verify that no errors occur if this method is called when catalog config is missing.
return args, kwargs """
CatalogIntegration.objects.all().delete()
def test_get_course_run(self, _mock_cache, mock_get_catalog_data):
course_run = factories.CourseRun()
mock_get_catalog_data.return_value = course_run
data = utils.get_course_run(self.course_key, self.user)
self.assert_contract(mock_get_catalog_data.call_args)
self.assertEqual(data, course_run)
def test_course_run_unavailable(self, _mock_cache, mock_get_catalog_data):
mock_get_catalog_data.return_value = []
data = utils.get_course_run(self.course_key, self.user)
self.assert_contract(mock_get_catalog_data.call_args) data = utils.get_course_runs([], self.user)
self.assertEqual(data, {}) self.assertEqual(data, {})
def test_cache_disabled(self, _mock_cache, mock_get_catalog_data): def test_get_course_run(self):
utils.get_course_run(self.course_key, self.user) course_keys = [self.course_key_1]
course_key_strings = [self.course_runs[0]["key"]]
self.register_catalog_course_run_response(course_key_strings, [self.course_runs[0]])
_, kwargs = self.assert_contract(mock_get_catalog_data.call_args) course_catalog_data_dict = utils.get_course_runs(course_keys, self.user)
expected_data = {self.course_runs[0]["key"]: self.course_runs[0]}
self.assertEqual(expected_data, course_catalog_data_dict)
self.assertIsNone(kwargs['cache_key']) def test_get_multiple_course_run(self):
course_key_strings = [self.course_runs[0]["key"], self.course_runs[1]["key"], self.course_runs[2]["key"]]
course_keys = [self.course_key_1, self.course_key_2, self.course_key_3]
self.register_catalog_course_run_response(
course_key_strings, [self.course_runs[0], self.course_runs[1], self.course_runs[2]]
)
def test_cache_enabled(self, _mock_cache, mock_get_catalog_data): course_catalog_data_dict = utils.get_course_runs(course_keys, self.user)
catalog_integration = self.create_catalog_integration(cache_ttl=1) expected_data = {
self.course_runs[0]["key"]: self.course_runs[0],
self.course_runs[1]["key"]: self.course_runs[1],
self.course_runs[2]["key"]: self.course_runs[2],
}
self.assertEqual(expected_data, course_catalog_data_dict)
def test_course_run_unavailable(self):
course_key_strings = [self.course_runs[0]["key"], self.course_runs[3]["key"]]
course_keys = [self.course_key_1, self.course_key_4]
self.register_catalog_course_run_response(course_key_strings, [self.course_runs[0]])
course_catalog_data_dict = utils.get_course_runs(course_keys, self.user)
expected_data = {self.course_runs[0]["key"]: self.course_runs[0]}
self.assertEqual(expected_data, course_catalog_data_dict)
def test_cached_course_run_data(self):
course_key_strings = [self.course_runs[0]["key"], self.course_runs[1]["key"]]
course_keys = [self.course_key_1, self.course_key_2]
course_cached_keys = [
"{}{}".format(utils.CatalogCacheUtility.CACHE_KEY_PREFIX, self.course_runs[0]["key"]),
"{}{}".format(utils.CatalogCacheUtility.CACHE_KEY_PREFIX, self.course_runs[1]["key"]),
]
self.register_catalog_course_run_response(course_key_strings, [self.course_runs[0], self.course_runs[1]])
expected_data = {
self.course_runs[0]["key"]: self.course_runs[0],
self.course_runs[1]["key"]: self.course_runs[1],
}
utils.get_course_run(self.course_key, self.user) course_catalog_data_dict = utils.get_course_runs(course_keys, self.user)
self.assertEqual(expected_data, course_catalog_data_dict)
cached_data = cache.get_many(course_cached_keys)
self.assertEqual(set(course_cached_keys), set(cached_data.keys()))
_, kwargs = mock_get_catalog_data.call_args with mock.patch('openedx.core.djangoapps.catalog.utils.get_edx_api_data') as mock_method:
course_catalog_data_dict = utils.get_course_runs(course_keys, self.user)
self.assertEqual(0, mock_method.call_count)
self.assertEqual(expected_data, course_catalog_data_dict)
self.assertEqual(kwargs['cache_key'], catalog_integration.CACHE_KEY)
def test_config_missing(self, _mock_cache, _mock_get_catalog_data): class TestGetRunMarketingUrl(TestCase, mixins.CatalogIntegrationMixin):
"""Verify that no errors occur if this method is called when catalog config is missing.""" """
CatalogIntegration.objects.all().delete() Tests covering retrieval of course run marketing URLs.
"""
data = utils.get_course_run(self.course_key, self.user)
self.assertEqual(data, {})
@mock.patch(UTILS_MODULE + '.get_course_run')
class TestGetRunMarketingUrl(TestCase):
"""Tests covering retrieval of course run marketing URLs."""
def setUp(self): def setUp(self):
super(TestGetRunMarketingUrl, self).setUp() super(TestGetRunMarketingUrl, self).setUp()
self.course_key = CourseKey.from_string('foo/bar/baz')
self.user = UserFactory() self.user = UserFactory()
self.course_runs = [factories.CourseRun() for __ in range(2)]
def test_get_run_marketing_url(self, mock_get_course_run): self.course_key_1 = CourseKey.from_string(self.course_runs[0]["key"])
course_run = factories.CourseRun()
mock_get_course_run.return_value = course_run def test_get_run_marketing_url(self):
with mock.patch('openedx.core.djangoapps.catalog.utils.get_course_runs', return_value={
url = utils.get_run_marketing_url(self.course_key, self.user) self.course_runs[0]["key"]: self.course_runs[0],
self.course_runs[1]["key"]: self.course_runs[1],
self.assertEqual(url, course_run['marketing_url']) }):
course_marketing_url = utils.get_run_marketing_url(self.course_key_1, self.user)
def test_marketing_url_missing(self, mock_get_course_run): self.assertEqual(self.course_runs[0]["marketing_url"], course_marketing_url)
mock_get_course_run.return_value = {}
def test_marketing_url_catalog_course_run_not_found(self):
url = utils.get_run_marketing_url(self.course_key, self.user) with mock.patch('openedx.core.djangoapps.catalog.utils.get_course_runs', return_value={
self.course_runs[0]["key"]: self.course_runs[0],
self.assertEqual(url, None) }):
course_marketing_url = utils.get_run_marketing_url(self.course_key_1, self.user)
self.assertEqual(self.course_runs[0]["marketing_url"], course_marketing_url)
def test_marketing_url_missing(self):
self.course_runs[1]["marketing_url"] = None
with mock.patch('openedx.core.djangoapps.catalog.utils.get_course_runs', return_value={
self.course_runs[0]["key"]: self.course_runs[0],
self.course_runs[1]["key"]: self.course_runs[1],
}):
course_marketing_url = utils.get_run_marketing_url(CourseKey.from_string("foo2/bar2/baz2"), self.user)
self.assertEqual(None, course_marketing_url)
class TestGetRunMarketingUrls(TestCase, mixins.CatalogIntegrationMixin):
"""
Tests covering retrieval of course run marketing URLs.
"""
def setUp(self):
super(TestGetRunMarketingUrls, self).setUp()
self.user = UserFactory()
self.course_runs = [factories.CourseRun() for __ in range(2)]
self.course_keys = [
CourseKey.from_string(self.course_runs[0]["key"]),
CourseKey.from_string(self.course_runs[1]["key"]),
]
def test_get_run_marketing_url(self):
expected_data = {
self.course_runs[0]["key"]: self.course_runs[0]["marketing_url"],
self.course_runs[1]["key"]: self.course_runs[1]["marketing_url"],
}
with mock.patch('openedx.core.djangoapps.catalog.utils.get_course_runs', return_value={
self.course_runs[0]["key"]: self.course_runs[0],
self.course_runs[1]["key"]: self.course_runs[1],
}):
course_marketing_url_dict = utils.get_run_marketing_urls(self.course_keys, self.user)
self.assertEqual(expected_data, course_marketing_url_dict)
def test_marketing_url_catalog_course_run_not_found(self):
expected_data = {
self.course_runs[0]["key"]: self.course_runs[0]["marketing_url"],
}
with mock.patch('openedx.core.djangoapps.catalog.utils.get_course_runs', return_value={
self.course_runs[0]["key"]: self.course_runs[0],
}):
course_marketing_url_dict = utils.get_run_marketing_urls(self.course_keys, self.user)
self.assertEqual(expected_data, course_marketing_url_dict)
def test_marketing_url_missing(self):
expected_data = {
self.course_runs[0]["key"]: self.course_runs[0]["marketing_url"],
self.course_runs[1]["key"]: None,
}
self.course_runs[1]["marketing_url"] = None
with mock.patch('openedx.core.djangoapps.catalog.utils.get_course_runs', return_value={
self.course_runs[0]["key"]: self.course_runs[0],
self.course_runs[1]["key"]: self.course_runs[1],
}):
course_marketing_url_dict = utils.get_run_marketing_urls(self.course_keys, self.user)
self.assertEqual(expected_data, course_marketing_url_dict)
"""Helper functions for working with the catalog service.""" """Helper functions for working with the catalog service."""
from urlparse import urlparse import abc
import logging
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from edx_rest_api_client.client import EdxRestApiClient from edx_rest_api_client.client import EdxRestApiClient
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
...@@ -10,6 +12,9 @@ from openedx.core.lib.edx_api_utils import get_edx_api_data ...@@ -10,6 +12,9 @@ from openedx.core.lib.edx_api_utils import get_edx_api_data
from openedx.core.lib.token_utils import JwtBuilder from openedx.core.lib.token_utils import JwtBuilder
log = logging.getLogger(__name__)
def create_catalog_api_client(user, catalog_integration): def create_catalog_api_client(user, catalog_integration):
"""Returns an API client which can be used to make catalog API requests.""" """Returns an API client which can be used to make catalog API requests."""
scopes = ['email', 'profile'] scopes = ['email', 'profile']
...@@ -115,38 +120,57 @@ def munge_catalog_program(catalog_program): ...@@ -115,38 +120,57 @@ def munge_catalog_program(catalog_program):
} }
def get_course_run(course_key, user): def get_and_cache_course_runs(course_keys, user):
"""Get a course run's data from the course catalog service.
Arguments:
course_key (CourseKey): Course key object identifying the run whose data we want.
user (User): The user to authenticate as when making requests to the catalog service.
Returns:
dict, empty if no data could be retrieved.
""" """
Get course run's data from the course catalog service and cache it.
"""
catalog_course_runs_against_course_keys = {}
catalog_integration = CatalogIntegration.current() catalog_integration = CatalogIntegration.current()
if catalog_integration.enabled: if catalog_integration.enabled:
api = create_catalog_api_client(user, catalog_integration) api = create_catalog_api_client(user, catalog_integration)
data = get_edx_api_data( catalog_data = get_edx_api_data(
catalog_integration, catalog_integration,
user, user,
'course_runs', 'course_runs',
resource_id=unicode(course_key),
cache_key=catalog_integration.CACHE_KEY if catalog_integration.is_cache_enabled else None,
api=api, api=api,
querystring={'exclude_utm': 1}, querystring={'keys': ",".join(course_keys), 'exclude_utm': 1},
) )
if catalog_data:
for catalog_course_run in catalog_data:
CatalogCacheUtility.cache_course_run(catalog_course_run)
catalog_course_runs_against_course_keys[catalog_course_run["key"]] = catalog_course_run
return catalog_course_runs_against_course_keys
return data if data else {}
else: def get_course_runs(course_keys, user):
return {} """
Get course run data from the course catalog service if not available in cache.
Arguments:
course_keys ([CourseKey]): A list of Course key object identifying the run whose data we want.
user (User): The user to authenticate as when making requests to the catalog service.
Returns:
dict of catalog course runs against course keys, empty if no data could be retrieved.
"""
catalog_course_runs_against_course_keys = CatalogCacheUtility.get_cached_catalog_course_runs(
course_keys
)
if len(catalog_course_runs_against_course_keys.keys()) != len(course_keys):
missing_course_keys = CatalogCacheUtility.get_course_keys_not_found_in_cache(
course_keys, catalog_course_runs_against_course_keys.keys()
)
catalog_course_runs_against_course_keys.update(
get_and_cache_course_runs(missing_course_keys, user)
)
return catalog_course_runs_against_course_keys
def get_run_marketing_url(course_key, user): def get_run_marketing_url(course_key, user):
"""Get a course run's marketing URL from the course catalog service. """
Get a course run's marketing URL from the course catalog service.
Arguments: Arguments:
course_key (CourseKey): Course key object identifying the run whose marketing URL we want. course_key (CourseKey): Course key object identifying the run whose marketing URL we want.
...@@ -155,5 +179,111 @@ def get_run_marketing_url(course_key, user): ...@@ -155,5 +179,111 @@ def get_run_marketing_url(course_key, user):
Returns: Returns:
string, the marketing URL, or None if no URL is available. string, the marketing URL, or None if no URL is available.
""" """
course_run = get_course_run(course_key, user) course_marketing_urls = get_run_marketing_urls([course_key], user)
return course_run.get('marketing_url') return course_marketing_urls.get(unicode(course_key))
def get_run_marketing_urls(course_keys, user):
"""
Get course run marketing URLs from the course catalog service against course keys.
Arguments:
course_keys ([CourseKey]): A list of Course key object identifying the run whose data we want.
user (User): The user to authenticate as when making requests to the catalog service.
Returns:
dict of run marketing URLs against course keys
"""
course_marketing_urls = {}
course_catalog_dict = get_course_runs(course_keys, user)
if not course_catalog_dict:
return course_marketing_urls
for course_key in course_keys:
course_key_string = unicode(course_key)
if course_key_string in course_catalog_dict:
course_marketing_urls[course_key_string] = course_catalog_dict[course_key_string].get('marketing_url')
return course_marketing_urls
class CatalogCacheUtility(object):
"""
Non-instantiatable class housing utility methods for caching catalog API data.
"""
__metaclass__ = abc.ABCMeta
CACHE_KEY_PREFIX = "catalog.course_runs."
@classmethod
def get_course_keys_not_found_in_cache(cls, course_keys, cached_course_run_keys):
"""
Get course key strings for which course run data is not available in cache.
Arguments:
course_key (CourseKey): CourseKey object to create corresponding catalog cache key.
Returns:
list of string rep of course keys not found in catalog cache.
"""
missing_course_keys = []
for course_key in course_keys:
course_key_string = unicode(course_key)
if course_key_string not in cached_course_run_keys:
missing_course_keys.append(course_key_string)
log.info("Cached catalog course run data missing for: '{}'".format(
", ".join(missing_course_keys)
))
return missing_course_keys
@classmethod
def get_cached_catalog_course_runs(cls, course_keys):
"""
Get course runs from cache against course keys.
Arguments:
course_keys ([CourseKey]): List of CourseKey object identifying the run whose data we want.
Returns:
dict of catalog course run against course key string
"""
course_catalog_run_cache_keys = [
cls._get_cache_key_name(course_key)
for course_key in course_keys
]
cached_catalog_course_runs = cache.get_many(course_catalog_run_cache_keys)
return {
cls._extract_course_key_from_cache_key_name(cached_key): cached_course_run
for cached_key, cached_course_run in cached_catalog_course_runs.iteritems()
}
@classmethod
def cache_course_run(cls, catalog_course_run):
"""
Caches catalog course run for course key.
"""
cache.set(
cls._get_cache_key_name(catalog_course_run["key"]),
catalog_course_run,
CatalogIntegration.current().cache_ttl
)
@classmethod
def _get_cache_key_name(cls, course_key):
"""
Returns key name to use to cache catalog course run data for course key.
Arguments:
course_key (CourseKey): CourseKey object to create corresponding catalog cache key.
Returns:
string, catalog cache key against course key.
"""
return "{}{}".format(cls.CACHE_KEY_PREFIX, unicode(course_key))
@classmethod
def _extract_course_key_from_cache_key_name(cls, catalog_course_run_cache_key):
"""
Returns course_key extracted from cache key of catalog course run data.
"""
return catalog_course_run_cache_key.replace(cls.CACHE_KEY_PREFIX, '')
...@@ -13,15 +13,16 @@ from django.test import TestCase ...@@ -13,15 +13,16 @@ from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils import timezone from django.utils import timezone
from django.utils.text import slugify from django.utils.text import slugify
from edx_oauth2_provider.tests.factories import ClientFactory
import httpretty import httpretty
import mock import mock
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from edx_oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL from provider.constants import CONFIDENTIAL
from lms.djangoapps.certificates.api import MODES from lms.djangoapps.certificates.api import MODES
from lms.djangoapps.commerce.tests.test_utils import update_commerce_config from lms.djangoapps.commerce.tests.test_utils import update_commerce_config
from openedx.core.djangoapps.catalog.tests import factories as catalog_factories
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.credentials.tests import factories as credentials_factories from openedx.core.djangoapps.credentials.tests import factories as credentials_factories
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin
...@@ -703,7 +704,9 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase): ...@@ -703,7 +704,9 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase):
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@mock.patch(UTILS_MODULE + '.get_run_marketing_url', mock.Mock(return_value=MARKETING_URL)) @mock.patch(UTILS_MODULE + '.get_run_marketing_url', mock.Mock(return_value=MARKETING_URL))
class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
"""Tests of the program data extender utility class.""" """
Tests of the program data extender utility class.
"""
maxDiff = None maxDiff = None
sku = 'abc123' sku = 'abc123'
password = 'test' password = 'test'
...@@ -721,6 +724,7 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -721,6 +724,7 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
self.course.start = timezone.now() - datetime.timedelta(days=1) self.course.start = timezone.now() - datetime.timedelta(days=1)
self.course.end = timezone.now() + datetime.timedelta(days=1) self.course.end = timezone.now() + datetime.timedelta(days=1)
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
self.course_id_string = unicode(self.course.id) # pylint: disable=no-member
self.organization = factories.Organization() self.organization = factories.Organization()
self.run_mode = factories.RunMode(course_key=unicode(self.course.id)) # pylint: disable=no-member self.run_mode = factories.RunMode(course_key=unicode(self.course.id)) # pylint: disable=no-member
...@@ -729,9 +733,12 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -729,9 +733,12 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
organizations=[self.organization], organizations=[self.organization],
course_codes=[self.course_code] course_codes=[self.course_code]
) )
self.course_run = catalog_factories.CourseRun(key=self.course_id_string)
def _assert_supplemented(self, actual, **kwargs): def _assert_supplemented(self, actual, **kwargs):
"""DRY helper used to verify that program data is extended correctly.""" """
DRY helper used to verify that program data is extended correctly.
"""
course_overview = CourseOverview.get_from_id(self.course.id) # pylint: disable=no-member course_overview = CourseOverview.get_from_id(self.course.id) # pylint: disable=no-member
run_mode = dict( run_mode = dict(
factories.RunMode( factories.RunMode(
...@@ -764,7 +771,9 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -764,7 +771,9 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
@ddt.unpack @ddt.unpack
@mock.patch(UTILS_MODULE + '.CourseMode.mode_for_course') @mock.patch(UTILS_MODULE + '.CourseMode.mode_for_course')
def test_student_enrollment_status(self, is_enrolled, enrolled_mode, is_upgrade_required, mock_get_mode): def test_student_enrollment_status(self, is_enrolled, enrolled_mode, is_upgrade_required, mock_get_mode):
"""Verify that program data is supplemented with the student's enrollment status.""" """
Verify that program data is supplemented with the student's enrollment status.
"""
expected_upgrade_url = '{root}/{path}?sku={sku}'.format( expected_upgrade_url = '{root}/{path}?sku={sku}'.format(
root=ECOMMERCE_URL_ROOT, root=ECOMMERCE_URL_ROOT,
path=self.checkout_path.strip('/'), path=self.checkout_path.strip('/'),
...@@ -790,7 +799,9 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -790,7 +799,9 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
@ddt.data(MODES.audit, MODES.verified) @ddt.data(MODES.audit, MODES.verified)
def test_inactive_enrollment_no_upgrade(self, enrolled_mode): def test_inactive_enrollment_no_upgrade(self, enrolled_mode):
"""Verify that a student with an inactive enrollment isn't encouraged to upgrade.""" """
Verify that a student with an inactive enrollment isn't encouraged to upgrade.
"""
update_commerce_config(enabled=True, checkout_page=self.checkout_path) update_commerce_config(enabled=True, checkout_page=self.checkout_path)
CourseEnrollmentFactory( CourseEnrollmentFactory(
...@@ -806,7 +817,9 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -806,7 +817,9 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
@mock.patch(UTILS_MODULE + '.CourseMode.mode_for_course') @mock.patch(UTILS_MODULE + '.CourseMode.mode_for_course')
def test_ecommerce_disabled(self, mock_get_mode): def test_ecommerce_disabled(self, mock_get_mode):
"""Verify that the utility can operate when the ecommerce service is disabled.""" """
Verify that the utility can operate when the ecommerce service is disabled.
"""
update_commerce_config(enabled=False, checkout_page=self.checkout_path) update_commerce_config(enabled=False, checkout_page=self.checkout_path)
mock_mode = mock.Mock() mock_mode = mock.Mock()
...@@ -825,7 +838,9 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -825,7 +838,9 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
) )
@ddt.unpack @ddt.unpack
def test_course_enrollment_status(self, start_offset, end_offset, is_enrollment_open): def test_course_enrollment_status(self, start_offset, end_offset, is_enrollment_open):
"""Verify that course enrollment status is reflected correctly.""" """
Verify that course enrollment status is reflected correctly.
"""
self.course.enrollment_start = timezone.now() - datetime.timedelta(days=start_offset) self.course.enrollment_start = timezone.now() - datetime.timedelta(days=start_offset)
self.course.enrollment_end = timezone.now() - datetime.timedelta(days=end_offset) self.course.enrollment_end = timezone.now() - datetime.timedelta(days=end_offset)
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
...@@ -839,7 +854,8 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -839,7 +854,8 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
) )
def test_no_enrollment_start_date(self): def test_no_enrollment_start_date(self):
"""Verify that a closed course with no explicit enrollment start date doesn't cause an error. """
Verify that a closed course with no explicit enrollment start date doesn't cause an error.
Regression test for ECOM-4973. Regression test for ECOM-4973.
""" """
...@@ -857,7 +873,9 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -857,7 +873,9 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
@mock.patch(UTILS_MODULE + '.certificate_api.certificate_downloadable_status') @mock.patch(UTILS_MODULE + '.certificate_api.certificate_downloadable_status')
@mock.patch(CERTIFICATES_API_MODULE + '.has_html_certificates_enabled') @mock.patch(CERTIFICATES_API_MODULE + '.has_html_certificates_enabled')
def test_certificate_url_retrieval(self, is_uuid_available, mock_html_certs_enabled, mock_get_cert_data): def test_certificate_url_retrieval(self, is_uuid_available, mock_html_certs_enabled, mock_get_cert_data):
"""Verify that the student's run mode certificate is included, when available.""" """
Verify that the student's run mode certificate is included, when available.
"""
test_uuid = uuid.uuid4().hex test_uuid = uuid.uuid4().hex
mock_get_cert_data.return_value = {'uuid': test_uuid} if is_uuid_available else {} mock_get_cert_data.return_value = {'uuid': test_uuid} if is_uuid_available else {}
mock_html_certs_enabled.return_value = True mock_html_certs_enabled.return_value = True
...@@ -882,7 +900,9 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -882,7 +900,9 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
@mock.patch(UTILS_MODULE + '.get_organization_by_short_name') @mock.patch(UTILS_MODULE + '.get_organization_by_short_name')
def test_organization_logo_exists(self, mock_get_organization_by_short_name): def test_organization_logo_exists(self, mock_get_organization_by_short_name):
""" Verify the logo image is set from the organizations api """ """
Verify the logo image is set from the organizations api.
"""
mock_logo_url = 'edx/logo.png' mock_logo_url = 'edx/logo.png'
mock_image = mock.Mock() mock_image = mock.Mock()
mock_image.url = mock_logo_url mock_image.url = mock_logo_url
...@@ -895,7 +915,9 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -895,7 +915,9 @@ class TestProgramDataExtender(ProgramsApiConfigMixin, ModuleStoreTestCase):
@mock.patch(UTILS_MODULE + '.get_organization_by_short_name') @mock.patch(UTILS_MODULE + '.get_organization_by_short_name')
def test_organization_missing(self, mock_get_organization_by_short_name): def test_organization_missing(self, mock_get_organization_by_short_name):
""" Verify the logo image is not set if the organizations api returns None """ """
Verify the logo image is not set if the organizations api returns None.
"""
mock_get_organization_by_short_name.return_value = None mock_get_organization_by_short_name.return_value = None
data = utils.ProgramDataExtender(self.program, self.user).extend() data = utils.ProgramDataExtender(self.program, self.user).extend()
self.assertEqual(data['organizations'][0].get('img'), None) self.assertEqual(data['organizations'][0].get('img'), None)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment