Commit 7e52af97 by Anthony Mangano

Render certificate templates in the correct language

LEARNER-2921
parent a285ed08
...@@ -503,7 +503,7 @@ def get_certificate_template(course_key, mode, language): ...@@ -503,7 +503,7 @@ def get_certificate_template(course_key, mode, language):
mode=mode mode=mode
) )
template = get_language_specific_template_or_default(language, mode_templates) template = get_language_specific_template_or_default(language, mode_templates)
return template.template if template else None return template if template else None
def get_language_specific_template_or_default(language, templates): def get_language_specific_template_or_default(language, templates):
...@@ -540,7 +540,12 @@ def _get_two_letter_language_code(language_code): ...@@ -540,7 +540,12 @@ def _get_two_letter_language_code(language_code):
Shortens language to only first two characters (e.g. es-419 becomes es) Shortens language to only first two characters (e.g. es-419 becomes es)
This is needed because Catalog returns locale language which is not always a 2 letter code. This is needed because Catalog returns locale language which is not always a 2 letter code.
""" """
return language_code[:2] if language_code else None if language_code is None:
return None
elif language_code == '':
return ''
else:
return language_code[:2]
def emit_certificate_event(event_name, user, course_id, course=None, event_data=None): def emit_certificate_event(event_name, user, course_id, course=None, event_data=None):
......
...@@ -41,6 +41,7 @@ from lms.djangoapps.badges.tests.factories import ( ...@@ -41,6 +41,7 @@ from lms.djangoapps.badges.tests.factories import (
) )
from lms.djangoapps.grades.tests.utils import mock_passing_grade from lms.djangoapps.grades.tests.utils import mock_passing_grade
from openedx.core.djangoapps.certificates.config import waffle from openedx.core.djangoapps.certificates.config import waffle
from openedx.core.djangoapps.dark_lang.models import DarkLangConfig
from openedx.core.lib.tests.assertions.events import assert_event_matches from openedx.core.lib.tests.assertions.events import assert_event_matches
from student.roles import CourseStaffRole from student.roles import CourseStaffRole
from student.tests.factories import CourseEnrollmentFactory, UserFactory from student.tests.factories import CourseEnrollmentFactory, UserFactory
...@@ -1079,6 +1080,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase): ...@@ -1079,6 +1080,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
Tests custom template search and rendering. Tests custom template search and rendering.
This test should check template matching when org={org}, course={course}, mode={mode}. This test should check template matching when org={org}, course={course}, mode={mode}.
""" """
DarkLangConfig(released_languages='es-419, fr', changed_by=self.user, enabled=True).save()
right_language = 'es' right_language = 'es'
wrong_language = 'fr' wrong_language = 'fr'
mock_get_org_id.return_value = 1 mock_get_org_id.return_value = 1
...@@ -1137,6 +1140,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase): ...@@ -1137,6 +1140,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
match org and mode. match org and mode.
This test should check template matching when org={org}, course=Null, mode={mode}. This test should check template matching when org={org}, course=Null, mode={mode}.
""" """
DarkLangConfig(released_languages='es-419, fr', changed_by=self.user, enabled=True).save()
right_language = 'es' right_language = 'es'
wrong_language = 'fr' wrong_language = 'fr'
mock_get_org_id.return_value = 1 mock_get_org_id.return_value = 1
...@@ -1193,6 +1198,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase): ...@@ -1193,6 +1198,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
Tests custom template search when we have a single template for a organization. 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. This test should check template matching when org={org}, course=Null, mode=null.
""" """
DarkLangConfig(released_languages='es-419, fr', changed_by=self.user, enabled=True).save()
right_language = 'es' right_language = 'es'
wrong_language = 'fr' wrong_language = 'fr'
mock_get_org_id.return_value = 1 mock_get_org_id.return_value = 1
...@@ -1248,6 +1255,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase): ...@@ -1248,6 +1255,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
Tests custom template search if we have a single template for a course mode. 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}. This test should check template matching when org=null, course=Null, mode={mode}.
""" """
DarkLangConfig(released_languages='es-419, fr', changed_by=self.user, enabled=True).save()
right_language = 'es' right_language = 'es'
wrong_language = 'fr' wrong_language = 'fr'
mock_get_org_id.return_value = 1 mock_get_org_id.return_value = 1
...@@ -1303,6 +1312,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase): ...@@ -1303,6 +1312,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
Tests custom template search if we have a single template for a course mode. 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}. This test should check template matching when org=null, course=Null, mode={mode}.
""" """
DarkLangConfig(released_languages='es-419, fr', changed_by=self.user, enabled=True).save()
right_language = 'es' right_language = 'es'
wrong_language = 'fr' wrong_language = 'fr'
mock_get_org_id.return_value = 1 mock_get_org_id.return_value = 1
......
...@@ -14,7 +14,7 @@ from django.contrib.auth.models import User ...@@ -14,7 +14,7 @@ from django.contrib.auth.models import User
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
from django.template import RequestContext from django.template import RequestContext
from django.utils.encoding import smart_str from django.utils.encoding import smart_str
from django.utils.translation import ugettext as _ from django.utils import translation
from eventtracking import tracker from eventtracking import tracker
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
...@@ -41,6 +41,7 @@ from courseware.courses import get_course_by_id ...@@ -41,6 +41,7 @@ from courseware.courses import get_course_by_id
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from edxmako.template import Template from edxmako.template import Template
from openedx.core.djangoapps.catalog.utils import get_course_run_details from openedx.core.djangoapps.catalog.utils import get_course_run_details
from openedx.core.djangoapps.lang_pref.api import released_languages
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.lib.courses import course_image_url from openedx.core.lib.courses import course_image_url
from openedx.core.djangoapps.certificates.api import display_date_for_certificate from openedx.core.djangoapps.certificates.api import display_date_for_certificate
...@@ -51,6 +52,10 @@ from util.views import handle_500 ...@@ -51,6 +52,10 @@ from util.views import handle_500
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
_ = translation.ugettext
INVALID_CERTIFICATE_TEMPLATE_PATH = 'certificates/invalid.html'
def get_certificate_description(mode, certificate_type, platform_name): def get_certificate_description(mode, certificate_type, platform_name):
...@@ -251,29 +256,6 @@ def _update_course_context(request, context, course, course_key, platform_name): ...@@ -251,29 +256,6 @@ def _update_course_context(request, context, course, course_key, platform_name):
platform_name=platform_name) platform_name=platform_name)
def _update_context_with_catalog_data(context, course_key):
"""
Updates context dictionary with relevant course run info from Discovery.
"""
course_certificate_settings = CertificateGenerationCourseSetting.get(course_key)
if course_certificate_settings:
course_run_fields = []
if course_certificate_settings.language_specific_templates_enabled:
course_run_fields.append('content_language')
if course_certificate_settings.include_hours_of_effort:
course_run_fields.extend(['weeks_to_complete', 'max_effort'])
if course_run_fields:
course_run_data = get_course_run_details(course_key, course_run_fields)
if course_run_data.get('weeks_to_complete') and course_run_data.get('max_effort'):
try:
weeks_to_complete = int(course_run_data['weeks_to_complete'])
max_effort = int(course_run_data['max_effort'])
context['hours_of_effort'] = weeks_to_complete * max_effort
except ValueError:
log.exception('Error occurred while parsing course run details')
context['content_language'] = course_run_data.get('content_language')
def _update_social_context(request, context, course, user, user_certificate, platform_name): def _update_social_context(request, context, course, user, user_certificate, platform_name):
""" """
Updates context dictionary with info required for social sharing. Updates context dictionary with info required for social sharing.
...@@ -432,26 +414,6 @@ def _track_certificate_events(request, context, course, user, user_certificate): ...@@ -432,26 +414,6 @@ def _track_certificate_events(request, context, course, user, user_certificate):
}) })
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, context.get('content_language'))
if custom_template:
template = Template(
custom_template,
output_encoding='utf-8',
input_encoding='utf-8',
default_filters=['decode.utf8'],
encoding_errors='replace',
)
context = RequestContext(request, context)
return HttpResponse(template.render(context))
return render_to_response("certificates/valid.html", context)
def _update_configuration_context(context, configuration): def _update_configuration_context(context, configuration):
""" """
Site Configuration will need to be able to override any hard coded Site Configuration will need to be able to override any hard coded
...@@ -535,14 +497,10 @@ def render_html_view(request, user_id, course_id): ...@@ -535,14 +497,10 @@ def render_html_view(request, user_id, course_id):
preview_mode = request.GET.get('preview', None) preview_mode = request.GET.get('preview', None)
platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME) platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME)
configuration = CertificateHtmlViewConfiguration.get_config() configuration = CertificateHtmlViewConfiguration.get_config()
# Create the initial view context, bootstrapping with Django settings and passed-in values
context = {}
_update_context_with_basic_info(context, course_id, platform_name, configuration)
invalid_template_path = 'certificates/invalid.html'
# Kick the user back to the "Invalid" screen if the feature is disabled globally # Kick the user back to the "Invalid" screen if the feature is disabled globally
if not settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False): if not settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False):
return render_to_response(invalid_template_path, context) return _render_invalid_certificate(course_id, platform_name, configuration)
# Load the course and user objects # Load the course and user objects
try: try:
...@@ -557,7 +515,7 @@ def render_html_view(request, user_id, course_id): ...@@ -557,7 +515,7 @@ def render_html_view(request, user_id, course_id):
"%d. Specific error: %s" "%d. Specific error: %s"
) )
log.info(error_str, course_id, user_id, str(exception)) log.info(error_str, course_id, user_id, str(exception))
return render_to_response(invalid_template_path, context) return _render_invalid_certificate(course_id, platform_name, configuration)
# Kick the user back to the "Invalid" screen if the feature is disabled for the course # Kick the user back to the "Invalid" screen if the feature is disabled for the course
if not course.cert_html_view_enabled: if not course.cert_html_view_enabled:
...@@ -566,7 +524,7 @@ def render_html_view(request, user_id, course_id): ...@@ -566,7 +524,7 @@ def render_html_view(request, user_id, course_id):
course_id, course_id,
user_id, user_id,
) )
return render_to_response(invalid_template_path, context) return _render_invalid_certificate(course_id, platform_name, configuration)
# Load user's certificate # Load user's certificate
user_certificate = _get_user_certificate(request, user, course_key, course, preview_mode) user_certificate = _get_user_certificate(request, user, course_key, course, preview_mode)
...@@ -576,7 +534,7 @@ def render_html_view(request, user_id, course_id): ...@@ -576,7 +534,7 @@ def render_html_view(request, user_id, course_id):
user_id, user_id,
course_id, course_id,
) )
return render_to_response(invalid_template_path, context) return _render_invalid_certificate(course_id, platform_name, configuration)
# Get the active certificate configuration for this course # Get the active certificate configuration for this course
# If we do not have an active certificate, we'll need to send the user to the "Invalid" screen # If we do not have an active certificate, we'll need to send the user to the "Invalid" screen
...@@ -588,46 +546,155 @@ def render_html_view(request, user_id, course_id): ...@@ -588,46 +546,155 @@ def render_html_view(request, user_id, course_id):
course_id, course_id,
user_id, user_id,
) )
return render_to_response(invalid_template_path, context) return _render_invalid_certificate(course_id, platform_name, configuration)
# Get data from Discovery service that will be necessary for rendering this Certificate.
catalog_data = _get_catalog_data_for_course(course_key)
# Determine whether to use the standard or custom template to render the certificate.
custom_template = None
custom_template_language = None
if settings.FEATURES.get('CUSTOM_CERTIFICATE_TEMPLATES_ENABLED', False):
custom_template, custom_template_language = _get_custom_template_and_language(
course.id,
user_certificate.mode,
catalog_data.pop('content_language', None)
)
# Determine the language that should be used to render the certificate.
# For the standard certificate template, use the user language. For custom templates, use
# the language associated with the template.
user_language = translation.get_language()
certificate_language = custom_template_language if custom_template else user_language
# Generate the certificate context in the correct language, then render the template.
with translation.override(certificate_language):
context = {'user_language': user_language}
context['certificate_data'] = active_configuration _update_context_with_basic_info(context, course_id, platform_name, configuration)
# Append/Override the existing view context values with any mode-specific ConfigurationModel values context['certificate_data'] = active_configuration
context.update(configuration.get(user_certificate.mode, {}))
# Append organization info # Append/Override the existing view context values with any mode-specific ConfigurationModel values
_update_organization_context(context, course) context.update(configuration.get(user_certificate.mode, {}))
# Append course info # Append organization info
_update_course_context(request, context, course, course_key, platform_name) _update_organization_context(context, course)
# Append course run info from discovery # Append course info
_update_context_with_catalog_data(context, course_key) _update_course_context(request, context, course, course_key, platform_name)
# Append user info # Append course run info from discovery
_update_context_with_user_info(context, user, user_certificate) context.update(catalog_data)
# Append social sharing info # Append user info
_update_social_context(request, context, course, user, user_certificate, platform_name) _update_context_with_user_info(context, user, user_certificate)
# Append/Override the existing view context values with certificate specific values # Append social sharing info
_update_certificate_context(context, course, user_certificate, platform_name) _update_social_context(request, context, course, user, user_certificate, platform_name)
# Append badge info # Append/Override the existing view context values with certificate specific values
_update_badge_context(context, course, user) _update_certificate_context(context, course, user_certificate, platform_name)
# Append site configuration overrides # Append badge info
_update_configuration_context(context, configuration) _update_badge_context(context, course, user)
# Add certificate header/footer data to current context # Append site configuration overrides
context.update(get_certificate_header_context(is_secure=request.is_secure())) _update_configuration_context(context, configuration)
context.update(get_certificate_footer_context())
# Append/Override the existing view context values with any course-specific static values from Advanced Settings # Add certificate header/footer data to current context
context.update(course.cert_html_view_overrides) context.update(get_certificate_header_context(is_secure=request.is_secure()))
context.update(get_certificate_footer_context())
# Track certificate view events # Append/Override the existing view context values with any course-specific static values from Advanced Settings
_track_certificate_events(request, context, course, user, user_certificate) context.update(course.cert_html_view_overrides)
# FINALLY, render appropriate certificate # Track certificate view events
return _render_certificate_template(request, context, course, user_certificate) _track_certificate_events(request, context, course, user, user_certificate)
# Render the certificate
return _render_valid_certificate(request, context, custom_template)
def _get_catalog_data_for_course(course_key):
"""
Retrieve data from the Discovery service necessary for rendering a certificate for a specific course.
"""
course_certificate_settings = CertificateGenerationCourseSetting.get(course_key)
if not course_certificate_settings:
return {}
catalog_data = {}
course_run_fields = []
if course_certificate_settings.language_specific_templates_enabled:
course_run_fields.append('content_language')
if course_certificate_settings.include_hours_of_effort:
course_run_fields.extend(['weeks_to_complete', 'max_effort'])
if course_run_fields:
course_run_data = get_course_run_details(course_key, course_run_fields)
if course_run_data.get('weeks_to_complete') and course_run_data.get('max_effort'):
try:
weeks_to_complete = int(course_run_data['weeks_to_complete'])
max_effort = int(course_run_data['max_effort'])
catalog_data['hours_of_effort'] = weeks_to_complete * max_effort
except ValueError:
log.exception('Error occurred while parsing course run details')
catalog_data['content_language'] = course_run_data.get('content_language')
return catalog_data
def _get_custom_template_and_language(course_id, course_mode, course_language):
"""
Return the custom certificate template, if any, that should be rendered for the provided course/mode/language
combination, along with the language that should be used to render that template.
"""
closest_released_language = _get_closest_released_language(course_language) if course_language else None
template = get_certificate_template(course_id, course_mode, closest_released_language)
if template and template.language:
return (template, closest_released_language)
elif template:
return (template, settings.LANGUAGE_CODE)
else:
return (None, None)
def _get_closest_released_language(target):
"""
Return the language code that most closely matches the target and is fully supported by the LMS, or None
if there are no fully supported languages that match the target.
"""
match = None
languages = released_languages()
for language in languages:
if language.code == target:
match = language.code
break
elif (match is None) and (language.code[:2] == target[:2]):
match = language.code
return match
def _render_invalid_certificate(course_id, platform_name, configuration):
context = {}
_update_context_with_basic_info(context, course_id, platform_name, configuration)
return render_to_response(INVALID_CERTIFICATE_TEMPLATE_PATH, context)
def _render_valid_certificate(request, context, custom_template=None):
if custom_template:
template = Template(
custom_template.template,
output_encoding='utf-8',
input_encoding='utf-8',
default_filters=['decode.utf8'],
encoding_errors='replace',
)
context = RequestContext(request, context)
return HttpResponse(template.render(context))
else:
return render_to_response("certificates/valid.html", context)
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