Flag for Hours of Effort in course certificates
Retrieve different data fields from Discovery
This commit is contained in:
@@ -503,7 +503,6 @@ def get_certificate_template(course_key, mode, language):
|
||||
mode=mode
|
||||
)
|
||||
template = get_language_specific_template_or_default(language, mode_templates)
|
||||
#return template[0].template if template else None
|
||||
return template.template if template else None
|
||||
|
||||
|
||||
@@ -532,6 +531,7 @@ def get_all_languages_or_default_template(templates):
|
||||
for template in templates:
|
||||
if template.language == '':
|
||||
return template
|
||||
|
||||
return templates[0] if templates else None
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('certificates', '0011_certificatetemplate_alter_unique'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='certificategenerationcoursesetting',
|
||||
name='include_hours_of_effort',
|
||||
field=models.NullBooleanField(default=None, help_text="Display estimated time to complete the course, which is equal to the maximum hours of effort per week times the length of the course in weeks. This attribute will only be displayed in a certificate when the attributes 'Weeks to complete' and 'Max effort' have been provided for the course run and its certificate template includes Hours of Effort."),
|
||||
),
|
||||
]
|
||||
@@ -870,11 +870,37 @@ class CertificateGenerationCourseSetting(TimeStampedModel):
|
||||
u"certificate template."
|
||||
)
|
||||
)
|
||||
include_hours_of_effort = models.NullBooleanField(
|
||||
default=None,
|
||||
help_text=(
|
||||
u"Display estimated time to complete the course, which is equal to the maximum hours of effort per week "
|
||||
u"times the length of the course in weeks. This attribute will only be displayed in a certificate when the "
|
||||
u"attributes 'Weeks to complete' and 'Max effort' have been provided for the course run and its certificate "
|
||||
u"template includes Hours of Effort."
|
||||
)
|
||||
)
|
||||
|
||||
class Meta(object):
|
||||
get_latest_by = 'created'
|
||||
app_label = "certificates"
|
||||
|
||||
@classmethod
|
||||
def get(cls, course_key):
|
||||
""" Retrieve certificate generation settings for a course.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): The identifier for the course.
|
||||
|
||||
Returns:
|
||||
CertificateGenerationCourseSetting
|
||||
"""
|
||||
try:
|
||||
latest = cls.objects.filter(course_key=course_key).latest()
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
else:
|
||||
return latest
|
||||
|
||||
@classmethod
|
||||
def is_self_generation_enabled_for_course(cls, course_key):
|
||||
"""Check whether self-generated certificates are enabled for a course.
|
||||
@@ -910,41 +936,6 @@ 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.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): The identifier for the course.
|
||||
|
||||
Returns:
|
||||
boolean
|
||||
|
||||
"""
|
||||
try:
|
||||
latest = cls.objects.filter(course_key=course_key).latest()
|
||||
except cls.DoesNotExist:
|
||||
return False
|
||||
else:
|
||||
return latest.language_specific_templates_enabled
|
||||
|
||||
@classmethod
|
||||
def set_language_specific_templates_enabled_for_course(cls, course_key, is_enabled):
|
||||
"""Enable or disable language-specific certificates for a course.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): The identifier for the course.
|
||||
is_enabled (boolean): Whether to enable or disable language-specific certificates.
|
||||
|
||||
"""
|
||||
default = {
|
||||
'language_specific_templates_enabled': is_enabled,
|
||||
}
|
||||
CertificateGenerationCourseSetting.objects.update_or_create(
|
||||
course_key=course_key,
|
||||
defaults=default
|
||||
)
|
||||
|
||||
|
||||
class CertificateGenerationConfiguration(ConfigurationModel):
|
||||
"""Configure certificate generation.
|
||||
|
||||
@@ -204,6 +204,37 @@ class CommonCertificatesTestCase(ModuleStoreTestCase):
|
||||
)
|
||||
template.save()
|
||||
|
||||
def _create_custom_template_with_hours_of_effort(self, org_id=None, mode=None, course_key=None, language=None):
|
||||
"""
|
||||
Creates a custom certificate template entry in DB that includes hours of effort.
|
||||
"""
|
||||
template_html = """
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<html>
|
||||
<body>
|
||||
lang: ${LANGUAGE_CODE}
|
||||
course name: ${accomplishment_copy_course_name}
|
||||
mode: ${course_mode}
|
||||
% if hours_of_effort:
|
||||
hours of effort: ${hours_of_effort}
|
||||
% endif
|
||||
${accomplishment_copy_course_description}
|
||||
${twitter_url}
|
||||
<img class="custom-logo" src="${static.certificate_asset_url('custom-logo')}" />
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
template = CertificateTemplate(
|
||||
name='custom template',
|
||||
template=template_html,
|
||||
organization_id=org_id,
|
||||
course_key=course_key,
|
||||
mode=mode,
|
||||
is_active=True,
|
||||
language=language
|
||||
)
|
||||
template.save()
|
||||
|
||||
|
||||
@attr(shard=1)
|
||||
@ddt.ddt
|
||||
@@ -216,8 +247,7 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
|
||||
super(CertificatesViewsTests, self).setUp()
|
||||
self.mock_course_run_details = {
|
||||
'content_language': 'en',
|
||||
'start': '2013-02-05T05:00:00Z',
|
||||
'end': '2013-03-05T05:00:00Z',
|
||||
'weeks_to_complete': '4',
|
||||
'max_effort': '10'
|
||||
}
|
||||
|
||||
@@ -1052,7 +1082,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
|
||||
course_run_details.update({'content_language': 'es'})
|
||||
mock_get_course_run_details.return_value = course_run_details
|
||||
|
||||
CertificateGenerationCourseSetting.set_language_specific_templates_enabled_for_course(self.course.id, True)
|
||||
CertificateGenerationCourseSetting.objects.update_or_create(
|
||||
course_key=self.course.id,
|
||||
defaults={
|
||||
'language_specific_templates_enabled': True
|
||||
}
|
||||
)
|
||||
|
||||
self._add_course_certificates(count=1, signatory_count=2)
|
||||
|
||||
@@ -1104,7 +1139,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
|
||||
course_run_details = self.mock_course_run_details
|
||||
course_run_details.update({'content_language': 'es'})
|
||||
mock_get_course_run_details.return_value = course_run_details
|
||||
CertificateGenerationCourseSetting.set_language_specific_templates_enabled_for_course(self.course.id, True)
|
||||
CertificateGenerationCourseSetting.objects.update_or_create(
|
||||
course_key=self.course.id,
|
||||
defaults={
|
||||
'language_specific_templates_enabled': True
|
||||
}
|
||||
)
|
||||
|
||||
self._add_course_certificates(count=1, signatory_count=2)
|
||||
|
||||
@@ -1155,7 +1195,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
|
||||
course_run_details = self.mock_course_run_details
|
||||
course_run_details.update({'content_language': 'es'})
|
||||
mock_get_course_run_details.return_value = course_run_details
|
||||
CertificateGenerationCourseSetting.set_language_specific_templates_enabled_for_course(self.course.id, True)
|
||||
CertificateGenerationCourseSetting.objects.update_or_create(
|
||||
course_key=self.course.id,
|
||||
defaults={
|
||||
'language_specific_templates_enabled': True
|
||||
}
|
||||
)
|
||||
|
||||
self._add_course_certificates(count=1, signatory_count=2)
|
||||
test_url = get_certificate_url(
|
||||
@@ -1205,7 +1250,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
|
||||
course_run_details = self.mock_course_run_details
|
||||
course_run_details.update({'content_language': 'es'})
|
||||
mock_get_course_run_details.return_value = course_run_details
|
||||
CertificateGenerationCourseSetting.set_language_specific_templates_enabled_for_course(self.course.id, True)
|
||||
CertificateGenerationCourseSetting.objects.update_or_create(
|
||||
course_key=self.course.id,
|
||||
defaults={
|
||||
'language_specific_templates_enabled': True
|
||||
}
|
||||
)
|
||||
|
||||
self._add_course_certificates(count=1, signatory_count=2)
|
||||
|
||||
@@ -1255,7 +1305,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
|
||||
course_run_details = self.mock_course_run_details
|
||||
course_run_details.update({'content_language': 'es-419'})
|
||||
mock_get_course_run_details.return_value = course_run_details
|
||||
CertificateGenerationCourseSetting.set_language_specific_templates_enabled_for_course(self.course.id, True)
|
||||
CertificateGenerationCourseSetting.objects.update_or_create(
|
||||
course_key=self.course.id,
|
||||
defaults={
|
||||
'language_specific_templates_enabled': True
|
||||
}
|
||||
)
|
||||
|
||||
self._add_course_certificates(count=1, signatory_count=2)
|
||||
|
||||
@@ -1291,6 +1346,36 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'course name: test_right_lang_template')
|
||||
|
||||
@override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED)
|
||||
@ddt.data(True, False)
|
||||
@patch('certificates.views.webview.get_course_run_details')
|
||||
@patch('certificates.api.get_course_organization_id')
|
||||
def test_certificate_custom_template_with_hours_of_effort(self, include_effort, mock_get_org_id, mock_get_course_run_details):
|
||||
"""
|
||||
Tests custom template properly retrieves and calculates Hours of Effort when the feature is enabled
|
||||
"""
|
||||
# mock the response data from Discovery that updates the context for template lookup and rendering
|
||||
mock_get_course_run_details.return_value = self.mock_course_run_details
|
||||
mock_get_org_id.return_value = 1
|
||||
CertificateGenerationCourseSetting.objects.update_or_create(
|
||||
course_key=self.course.id,
|
||||
defaults={
|
||||
'include_hours_of_effort': include_effort
|
||||
}
|
||||
)
|
||||
self._add_course_certificates(count=1, signatory_count=2)
|
||||
self._create_custom_template_with_hours_of_effort(org_id=1, language=None)
|
||||
test_url = get_certificate_url(
|
||||
user_id=self.user.id,
|
||||
course_id=unicode(self.course.id)
|
||||
)
|
||||
response = self.client.get(test_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
if include_effort:
|
||||
self.assertIn('hours of effort: 40', response.content)
|
||||
else:
|
||||
self.assertNotIn('hours of effort', response.content)
|
||||
|
||||
@ddt.data(True, False)
|
||||
@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):
|
||||
|
||||
@@ -249,20 +249,29 @@ def _update_course_context(request, context, course, course_key, 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', 'content_language']
|
||||
course_run_data = get_course_run_details(course_key, fields)
|
||||
if course_run_data.get('start') and course_run_data.get('end') and course_run_data.get('max_effort'):
|
||||
# Calculate duration of the course run in weeks, multiplied by max_effort for total Hours of Effort
|
||||
try:
|
||||
start = parser.parse(course_run_data.get('start'))
|
||||
end = parser.parse(course_run_data.get('end'))
|
||||
max_effort = int(course_run_data.get('max_effort'))
|
||||
context['hours_of_effort'] = ((end - start).days / 7) * max_effort
|
||||
except ValueError:
|
||||
log.exception('Error occurred while parsing course run details')
|
||||
context['content_language'] = course_run_data.get('content_language')
|
||||
|
||||
|
||||
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):
|
||||
@@ -592,6 +601,9 @@ def render_html_view(request, user_id, course_id):
|
||||
# Append course info
|
||||
_update_course_context(request, context, course, course_key, platform_name)
|
||||
|
||||
# Append course run info from discovery
|
||||
_update_context_with_catalog_data(context, course_key)
|
||||
|
||||
# Append user info
|
||||
_update_context_with_user_info(context, user, user_certificate)
|
||||
|
||||
|
||||
@@ -119,7 +119,8 @@ class CourseRunFactory(DictFactoryBase):
|
||||
type = 'verified'
|
||||
uuid = factory.Faker('uuid4')
|
||||
content_language = 'en'
|
||||
max_effort = 5
|
||||
max_effort = 4
|
||||
weeks_to_complete = 10
|
||||
|
||||
|
||||
class CourseFactory(DictFactoryBase):
|
||||
|
||||
@@ -349,11 +349,10 @@ class TestGetCourseRunDetails(CatalogIntegrationMixin, TestCase):
|
||||
course_run = CourseRunFactory()
|
||||
course_run_details = {
|
||||
'content_language': course_run['content_language'],
|
||||
'start': course_run['start'],
|
||||
'end': course_run['end'],
|
||||
'weeks_to_complete': course_run['weeks_to_complete'],
|
||||
'max_effort': course_run['max_effort']
|
||||
}
|
||||
mock_get_edx_api_data.return_value = course_run_details
|
||||
data = get_course_run_details(course_run['key'], ['content_language', 'start', 'end', 'max_effort'])
|
||||
data = get_course_run_details(course_run['key'], ['content_language', 'weeks_to_complete', 'max_effort'])
|
||||
self.assertTrue(mock_get_edx_api_data.called)
|
||||
self.assertEqual(data, course_run_details)
|
||||
|
||||
Reference in New Issue
Block a user