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):
mode=mode
)
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):
......@@ -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)
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):
......
......@@ -41,6 +41,7 @@ from lms.djangoapps.badges.tests.factories import (
)
from lms.djangoapps.grades.tests.utils import mock_passing_grade
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 student.roles import CourseStaffRole
from student.tests.factories import CourseEnrollmentFactory, UserFactory
......@@ -1079,6 +1080,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
Tests custom template search and rendering.
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'
wrong_language = 'fr'
mock_get_org_id.return_value = 1
......@@ -1137,6 +1140,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
match org and 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'
wrong_language = 'fr'
mock_get_org_id.return_value = 1
......@@ -1193,6 +1198,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
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.
"""
DarkLangConfig(released_languages='es-419, fr', changed_by=self.user, enabled=True).save()
right_language = 'es'
wrong_language = 'fr'
mock_get_org_id.return_value = 1
......@@ -1248,6 +1255,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
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}.
"""
DarkLangConfig(released_languages='es-419, fr', changed_by=self.user, enabled=True).save()
right_language = 'es'
wrong_language = 'fr'
mock_get_org_id.return_value = 1
......@@ -1303,6 +1312,8 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
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}.
"""
DarkLangConfig(released_languages='es-419, fr', changed_by=self.user, enabled=True).save()
right_language = 'es'
wrong_language = 'fr'
mock_get_org_id.return_value = 1
......
......@@ -14,7 +14,7 @@ from django.contrib.auth.models import User
from django.http import Http404, HttpResponse
from django.template import RequestContext
from django.utils.encoding import smart_str
from django.utils.translation import ugettext as _
from django.utils import translation
from eventtracking import tracker
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
......@@ -41,6 +41,7 @@ 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.lang_pref.api import released_languages
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
......@@ -51,6 +52,10 @@ from util.views import handle_500
log = logging.getLogger(__name__)
_ = translation.ugettext
INVALID_CERTIFICATE_TEMPLATE_PATH = 'certificates/invalid.html'
def get_certificate_description(mode, certificate_type, platform_name):
......@@ -251,29 +256,6 @@ def _update_course_context(request, context, course, course_key, 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):
"""
Updates context dictionary with info required for social sharing.
......@@ -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):
"""
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):
preview_mode = request.GET.get('preview', None)
platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME)
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
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
try:
......@@ -557,7 +515,7 @@ def render_html_view(request, user_id, course_id):
"%d. Specific error: %s"
)
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
if not course.cert_html_view_enabled:
......@@ -566,7 +524,7 @@ def render_html_view(request, user_id, course_id):
course_id,
user_id,
)
return render_to_response(invalid_template_path, context)
return _render_invalid_certificate(course_id, platform_name, configuration)
# Load user's certificate
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):
user_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
# 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):
course_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.update(configuration.get(user_certificate.mode, {}))
context['certificate_data'] = active_configuration
# Append organization info
_update_organization_context(context, course)
# Append/Override the existing view context values with any mode-specific ConfigurationModel values
context.update(configuration.get(user_certificate.mode, {}))
# Append course info
_update_course_context(request, context, course, course_key, platform_name)
# Append organization info
_update_organization_context(context, course)
# Append course run info from discovery
_update_context_with_catalog_data(context, course_key)
# Append course info
_update_course_context(request, context, course, course_key, platform_name)
# Append user info
_update_context_with_user_info(context, user, user_certificate)
# Append course run info from discovery
context.update(catalog_data)
# Append social sharing info
_update_social_context(request, context, course, user, user_certificate, platform_name)
# Append user info
_update_context_with_user_info(context, user, user_certificate)
# Append/Override the existing view context values with certificate specific values
_update_certificate_context(context, course, user_certificate, platform_name)
# Append social sharing info
_update_social_context(request, context, course, user, user_certificate, platform_name)
# Append badge info
_update_badge_context(context, course, user)
# Append/Override the existing view context values with certificate specific values
_update_certificate_context(context, course, user_certificate, platform_name)
# Append site configuration overrides
_update_configuration_context(context, configuration)
# Append badge info
_update_badge_context(context, course, user)
# Add certificate header/footer data to current context
context.update(get_certificate_header_context(is_secure=request.is_secure()))
context.update(get_certificate_footer_context())
# Append site configuration overrides
_update_configuration_context(context, configuration)
# Append/Override the existing view context values with any course-specific static values from Advanced Settings
context.update(course.cert_html_view_overrides)
# Add certificate header/footer data to current context
context.update(get_certificate_header_context(is_secure=request.is_secure()))
context.update(get_certificate_footer_context())
# Track certificate view events
_track_certificate_events(request, context, course, user, user_certificate)
# Append/Override the existing view context values with any course-specific static values from Advanced Settings
context.update(course.cert_html_view_overrides)
# FINALLY, render appropriate certificate
return _render_certificate_template(request, context, course, user_certificate)
# Track certificate view events
_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