diff --git a/lms/djangoapps/certificates/api.py b/lms/djangoapps/certificates/api.py index 8450f14fa9..d480ca9073 100644 --- a/lms/djangoapps/certificates/api.py +++ b/lms/djangoapps/certificates/api.py @@ -484,9 +484,9 @@ def get_active_web_certificate(course, is_preview_mode=None): return None -def get_certificate_template(course_key, mode): +def get_certificate_template(course_key, mode, language): # pylint: disable=unused-argument """ - Retrieves the custom certificate template based on course_key and mode. + Retrieves the custom certificate template based on course_key, mode, and language. """ org_id, template = None, None # fetch organization of the course diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index f356338d51..613fa69451 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -910,6 +910,7 @@ class CertificateGenerationCourseSetting(TimeStampedModel): defaults=default ) + @classmethod def is_language_specific_templates_enabled_for_course(cls, course_key): """Check whether language-specific certificates are enabled for a course. diff --git a/lms/djangoapps/certificates/tests/test_webview_views.py b/lms/djangoapps/certificates/tests/test_webview_views.py index e28529a83b..f14f907b67 100644 --- a/lms/djangoapps/certificates/tests/test_webview_views.py +++ b/lms/djangoapps/certificates/tests/test_webview_views.py @@ -13,7 +13,7 @@ from django.core.urlresolvers import reverse from django.test.client import Client, RequestFactory from django.test.utils import override_settings from util.date_utils import strftime_localized -from mock import patch +from mock import Mock, patch from nose.plugins.attrib import attr from certificates.api import get_certificate_url @@ -884,11 +884,13 @@ class CertificatesViewsTests(CommonCertificatesTestCase): @override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED) @override_settings(LANGUAGE_CODE='fr') - def test_certificate_custom_template_with_org_mode_course(self): + @patch('certificates.views.webview.get_course_run_details') + def test_certificate_custom_template_with_org_mode_course(self, mock_get_course_run_details): """ Tests custom template search and rendering. This test should check template matching when org={org}, course={course}, mode={mode}. """ + mock_get_course_run_details.return_value = {'language': 'en'} self._add_course_certificates(count=1, signatory_count=2) self._create_custom_template(org_id=1, mode='honor', course_key=unicode(self.course.id)) self._create_custom_template(org_id=2, mode='honor') @@ -913,12 +915,14 @@ class CertificatesViewsTests(CommonCertificatesTestCase): self.assertContains(response, 'course name: course_title_0') @override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED) - def test_certificate_custom_template_with_org(self): + @patch('certificates.views.webview.get_course_run_details') + def test_certificate_custom_template_with_org(self, mock_get_course_run_details): """ Tests custom template search if we have a single template for organization and mode with course set to Null. This test should check template matching when org={org}, course=Null, mode={mode}. """ + mock_get_course_run_details.return_value = {'language': 'en'} course = CourseFactory.create( org='cstX', number='cst_22', display_name='custom template course' ) @@ -940,11 +944,13 @@ class CertificatesViewsTests(CommonCertificatesTestCase): self.assertContains(response, 'course name: course_title_0') @override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED) - def test_certificate_custom_template_with_organization(self): + @patch('certificates.views.webview.get_course_run_details') + def test_certificate_custom_template_with_organization(self, mock_get_course_run_details): """ Tests custom template search when we have a single template for a organization. This test should check template matching when org={org}, course=Null, mode=null. """ + mock_get_course_run_details.return_value = {'language': 'en'} self._add_course_certificates(count=1, signatory_count=2) self._create_custom_template(org_id=1, mode='honor') self._create_custom_template(org_id=1, mode='honor', course_key=self.course.id) @@ -962,11 +968,13 @@ class CertificatesViewsTests(CommonCertificatesTestCase): self.assertEqual(response.status_code, 200) @override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED) - def test_certificate_custom_template_with_course_mode(self): + @patch('certificates.views.webview.get_course_run_details') + def test_certificate_custom_template_with_course_mode(self, mock_get_course_run_details): """ Tests custom template search if we have a single template for a course mode. This test should check template matching when org=null, course=Null, mode={mode}. """ + mock_get_course_run_details.return_value = {'language': 'en'} mode = 'honor' self._add_course_certificates(count=1, signatory_count=2) self._create_custom_template(mode=mode) @@ -982,10 +990,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase): self.assertContains(response, 'mode: {}'.format(mode)) @ddt.data(True, False) - def test_certificate_custom_template_with_unicode_data(self, custom_certs_enabled): + @patch('certificates.views.webview.get_course_run_details') + def test_certificate_custom_template_with_unicode_data(self, custom_certs_enabled, mock_get_course_run_details): """ Tests custom template renders properly with unicode data. """ + mock_get_course_run_details.return_value = {'language': 'en'} mode = 'honor' self._add_course_certificates(count=1, signatory_count=2) self._create_custom_template(mode=mode) @@ -1014,10 +1024,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase): self.assertContains(response, 'https://twitter.com/intent/tweet') @override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED) - def test_certificate_asset_by_slug(self): + @patch('certificates.views.webview.get_course_run_details') + def test_certificate_asset_by_slug(self, mock_get_course_run_details): """ Tests certificate template asset display by slug using static.certificate_asset_url method. """ + mock_get_course_run_details.return_value = {'language': 'en'} self._add_course_certificates(count=1, signatory_count=2) self._create_custom_template(mode='honor') test_url = get_certificate_url( diff --git a/lms/djangoapps/certificates/views/webview.py b/lms/djangoapps/certificates/views/webview.py index 8098b3258d..e9cd754eb9 100644 --- a/lms/djangoapps/certificates/views/webview.py +++ b/lms/djangoapps/certificates/views/webview.py @@ -30,6 +30,7 @@ from certificates.api import ( has_html_certificates_enabled ) from certificates.models import ( + CertificateGenerationCourseSetting, CertificateHtmlViewConfiguration, CertificateSocialNetworks, CertificateStatuses, @@ -39,6 +40,7 @@ from courseware.access import has_access from courseware.courses import get_course_by_id from edxmako.shortcuts import render_to_response from edxmako.template import Template +from openedx.core.djangoapps.catalog.utils import get_course_run_details from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.lib.courses import course_image_url from openedx.core.djangoapps.certificates.api import display_date_for_certificate @@ -224,7 +226,7 @@ def _update_context_with_basic_info(context, course_id, platform_name, configura ) -def _update_course_context(request, context, course, platform_name): +def _update_course_context(request, context, course, course_key, platform_name): """ Updates context dictionary with course info. """ @@ -248,6 +250,11 @@ def _update_course_context(request, context, course, platform_name): '{partner_short_name}.').format( partner_short_name=context['organization_short_name'], platform_name=platform_name) + # If language specific templates are enabled for the course, add course_run specific information to the context + if CertificateGenerationCourseSetting.is_language_specific_templates_enabled_for_course(course_key): + fields = ['start', 'end', 'max_effort', 'language'] + course_run_data = get_course_run_details(course_key, fields) + context.update(course_run_data) def _update_social_context(request, context, course, user, user_certificate, platform_name): @@ -413,7 +420,7 @@ def _render_certificate_template(request, context, course, user_certificate): Picks appropriate certificate templates and renders it. """ if settings.FEATURES.get('CUSTOM_CERTIFICATE_TEMPLATES_ENABLED', False): - custom_template = get_certificate_template(course.id, user_certificate.mode) + custom_template = get_certificate_template(course.id, user_certificate.mode, context.get('language')) if custom_template: template = Template( custom_template, @@ -571,7 +578,7 @@ def render_html_view(request, user_id, course_id): _update_organization_context(context, course) # Append course info - _update_course_context(request, context, course, platform_name) + _update_course_context(request, context, course, course_key, platform_name) # Append user info _update_context_with_user_info(context, user, user_certificate) diff --git a/openedx/core/djangoapps/catalog/tests/factories.py b/openedx/core/djangoapps/catalog/tests/factories.py index 69c0906d38..0ad304b426 100644 --- a/openedx/core/djangoapps/catalog/tests/factories.py +++ b/openedx/core/djangoapps/catalog/tests/factories.py @@ -118,6 +118,8 @@ class CourseRunFactory(DictFactoryBase): title = factory.Faker('catch_phrase') type = 'verified' uuid = factory.Faker('uuid4') + language = 'en' + max_effort = 5 class CourseFactory(DictFactoryBase): diff --git a/openedx/core/djangoapps/catalog/tests/test_utils.py b/openedx/core/djangoapps/catalog/tests/test_utils.py index cb59c44f12..ad4a3d7bf0 100644 --- a/openedx/core/djangoapps/catalog/tests/test_utils.py +++ b/openedx/core/djangoapps/catalog/tests/test_utils.py @@ -15,6 +15,7 @@ from openedx.core.djangoapps.catalog.tests.factories import CourseRunFactory, Pr from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin from openedx.core.djangoapps.catalog.utils import ( get_course_runs, + get_course_run_details, get_program_types, get_programs, get_programs_with_type @@ -304,3 +305,31 @@ class TestGetCourseRuns(CatalogIntegrationMixin, TestCase): self.assertTrue(mock_get_edx_api_data.called) self.assert_contract(mock_get_edx_api_data.call_args) self.assertEqual(data, catalog_course_runs) + + +@skip_unless_lms +@mock.patch(UTILS_MODULE + '.get_edx_api_data') +class TestGetCourseRunDetails(CatalogIntegrationMixin, TestCase): + """ + Tests covering retrieval of information about a specific course run from the catalog service. + """ + def setUp(self): + super(TestGetCourseRunDetails, self).setUp() + self.catalog_integration = self.create_catalog_integration(cache_ttl=1) + self.user = UserFactory(username=self.catalog_integration.service_username) + + def test_get_course_run_details(self, mock_get_edx_api_data): + """ + Test retrieval of details about a specific course run + """ + course_run = CourseRunFactory() + course_run_details = { + 'language': course_run['language'], + 'start': course_run['start'], + 'end': course_run['end'], + 'max_effort': course_run['max_effort'] + } + mock_get_edx_api_data.return_value = course_run_details + data = get_course_run_details(course_run['key'], ['language', 'start', 'end', 'max_effort']) + self.assertTrue(mock_get_edx_api_data.called) + self.assertEqual(data, course_run_details) diff --git a/openedx/core/djangoapps/catalog/utils.py b/openedx/core/djangoapps/catalog/utils.py index 3386b805dc..efa4951bce 100644 --- a/openedx/core/djangoapps/catalog/utils.py +++ b/openedx/core/djangoapps/catalog/utils.py @@ -185,3 +185,39 @@ def get_course_runs(): course_runs = get_edx_api_data(catalog_integration, 'course_runs', api=api, querystring=querystring) return course_runs + + +def get_course_run_details(course_run_key, fields): + """ + Retrieve information about the course run with the given id + + Arguments: + course_run_key: key for the course_run about which we are retrieving information + + Returns: + dict with language, start date, end date, and max_effort details about specified course run + """ + catalog_integration = CatalogIntegration.current() + course_run_details = dict() + if catalog_integration.enabled: + try: + user = catalog_integration.get_service_user() + except ObjectDoesNotExist: + msg = 'Catalog service user {} does not exist. Data for course_run {} will not be retrieved'.format( + catalog_integration.service_username, + course_run_key + ) + logger.error(msg) + return course_run_details + api = create_catalog_api_client(user) + + cache_key = '{base}.course_runs'.format(base=catalog_integration.CACHE_KEY) + + course_run_details = get_edx_api_data(catalog_integration, 'course_runs', api, resource_id=course_run_key, + cache_key=cache_key, many=False, traverse_pagination=False, fields=fields) + else: + msg = 'Unable to retrieve details about course_run {} because Catalog Integration is not enabled'.format( + course_run_key + ) + logger.error(msg) + return course_run_details diff --git a/openedx/core/lib/edx_api_utils.py b/openedx/core/lib/edx_api_utils.py index ce9e9d85ba..f10f1030df 100644 --- a/openedx/core/lib/edx_api_utils.py +++ b/openedx/core/lib/edx_api_utils.py @@ -15,8 +15,20 @@ from openedx.core.lib.token_utils import JwtBuilder log = logging.getLogger(__name__) +def get_fields(fields, response): + """Extracts desired fields from the API response""" + results = {} + for field in fields: + try: + results[field] = response[field] + # TODO: Determine what exception would be raised here if response does not have the specified field + except: + msg = '{resource} does not have the attribute {field}'.format(resource, field) + log.exception(msg) + + def get_edx_api_data(api_config, resource, api, resource_id=None, querystring=None, cache_key=None, many=True, - traverse_pagination=True): + traverse_pagination=True, fields=None): """GET data from an edX REST API. DRY utility for handling caching and pagination. @@ -59,7 +71,10 @@ def get_edx_api_data(api_config, resource, api, resource_id=None, querystring=No response = endpoint(resource_id).get(**querystring) if resource_id is not None: - results = response + if fields: + results = get_fields(fields, response) + else: + results = response elif traverse_pagination: results = _traverse_pagination(response, endpoint, querystring, no_data) else: