Merge pull request #16011 from edx/McKenzieW/learner-2457
Added util method to retrieve individual course_run data from Catalog
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user