Commit 39bf59e6 by McKenzie Welter

Flag for Hours of Effort in course certificates

Retrieve different data fields from Discovery
parent 7382fe12
...@@ -503,7 +503,6 @@ def get_certificate_template(course_key, mode, language): ...@@ -503,7 +503,6 @@ 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[0].template if template else None
return template.template if template else None return template.template if template else None
...@@ -532,6 +531,7 @@ def get_all_languages_or_default_template(templates): ...@@ -532,6 +531,7 @@ def get_all_languages_or_default_template(templates):
for template in templates: for template in templates:
if template.language == '': if template.language == '':
return template return template
return templates[0] if templates else None return templates[0] if templates else None
......
# -*- 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,49 +870,40 @@ class CertificateGenerationCourseSetting(TimeStampedModel): ...@@ -870,49 +870,40 @@ class CertificateGenerationCourseSetting(TimeStampedModel):
u"certificate template." 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): class Meta(object):
get_latest_by = 'created' get_latest_by = 'created'
app_label = "certificates" app_label = "certificates"
@classmethod @classmethod
def is_self_generation_enabled_for_course(cls, course_key): def get(cls, course_key):
"""Check whether self-generated certificates are enabled for a course. """ Retrieve certificate generation settings for a course.
Arguments: Arguments:
course_key (CourseKey): The identifier for the course. course_key (CourseKey): The identifier for the course.
Returns: Returns:
boolean CertificateGenerationCourseSetting
""" """
try: try:
latest = cls.objects.filter(course_key=course_key).latest() latest = cls.objects.filter(course_key=course_key).latest()
except cls.DoesNotExist: except cls.DoesNotExist:
return False return None
else: else:
return latest.self_generation_enabled return latest
@classmethod @classmethod
def set_self_generatation_enabled_for_course(cls, course_key, is_enabled): def is_self_generation_enabled_for_course(cls, course_key):
"""Enable or disable self-generated certificates for a course. """Check whether self-generated certificates are enabled for a course.
Arguments:
course_key (CourseKey): The identifier for the course.
is_enabled (boolean): Whether to enable or disable self-generated certificates.
"""
default = {
'self_generation_enabled': is_enabled
}
CertificateGenerationCourseSetting.objects.update_or_create(
course_key=course_key,
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: Arguments:
course_key (CourseKey): The identifier for the course. course_key (CourseKey): The identifier for the course.
...@@ -926,19 +917,19 @@ class CertificateGenerationCourseSetting(TimeStampedModel): ...@@ -926,19 +917,19 @@ class CertificateGenerationCourseSetting(TimeStampedModel):
except cls.DoesNotExist: except cls.DoesNotExist:
return False return False
else: else:
return latest.language_specific_templates_enabled return latest.self_generation_enabled
@classmethod @classmethod
def set_language_specific_templates_enabled_for_course(cls, course_key, is_enabled): def set_self_generatation_enabled_for_course(cls, course_key, is_enabled):
"""Enable or disable language-specific certificates for a course. """Enable or disable self-generated certificates for a course.
Arguments: Arguments:
course_key (CourseKey): The identifier for the course. course_key (CourseKey): The identifier for the course.
is_enabled (boolean): Whether to enable or disable language-specific certificates. is_enabled (boolean): Whether to enable or disable self-generated certificates.
""" """
default = { default = {
'language_specific_templates_enabled': is_enabled, 'self_generation_enabled': is_enabled
} }
CertificateGenerationCourseSetting.objects.update_or_create( CertificateGenerationCourseSetting.objects.update_or_create(
course_key=course_key, course_key=course_key,
......
...@@ -204,6 +204,37 @@ class CommonCertificatesTestCase(ModuleStoreTestCase): ...@@ -204,6 +204,37 @@ class CommonCertificatesTestCase(ModuleStoreTestCase):
) )
template.save() 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) @attr(shard=1)
@ddt.ddt @ddt.ddt
...@@ -216,8 +247,7 @@ class CertificatesViewsTests(CommonCertificatesTestCase): ...@@ -216,8 +247,7 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
super(CertificatesViewsTests, self).setUp() super(CertificatesViewsTests, self).setUp()
self.mock_course_run_details = { self.mock_course_run_details = {
'content_language': 'en', 'content_language': 'en',
'start': '2013-02-05T05:00:00Z', 'weeks_to_complete': '4',
'end': '2013-03-05T05:00:00Z',
'max_effort': '10' 'max_effort': '10'
} }
...@@ -1052,7 +1082,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase): ...@@ -1052,7 +1082,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
course_run_details.update({'content_language': 'es'}) course_run_details.update({'content_language': 'es'})
mock_get_course_run_details.return_value = course_run_details 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) self._add_course_certificates(count=1, signatory_count=2)
...@@ -1104,7 +1139,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase): ...@@ -1104,7 +1139,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
course_run_details = self.mock_course_run_details course_run_details = self.mock_course_run_details
course_run_details.update({'content_language': 'es'}) course_run_details.update({'content_language': 'es'})
mock_get_course_run_details.return_value = course_run_details 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) self._add_course_certificates(count=1, signatory_count=2)
...@@ -1155,7 +1195,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase): ...@@ -1155,7 +1195,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
course_run_details = self.mock_course_run_details course_run_details = self.mock_course_run_details
course_run_details.update({'content_language': 'es'}) course_run_details.update({'content_language': 'es'})
mock_get_course_run_details.return_value = course_run_details 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) self._add_course_certificates(count=1, signatory_count=2)
test_url = get_certificate_url( test_url = get_certificate_url(
...@@ -1205,7 +1250,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase): ...@@ -1205,7 +1250,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
course_run_details = self.mock_course_run_details course_run_details = self.mock_course_run_details
course_run_details.update({'content_language': 'es'}) course_run_details.update({'content_language': 'es'})
mock_get_course_run_details.return_value = course_run_details 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) self._add_course_certificates(count=1, signatory_count=2)
...@@ -1255,7 +1305,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase): ...@@ -1255,7 +1305,12 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
course_run_details = self.mock_course_run_details course_run_details = self.mock_course_run_details
course_run_details.update({'content_language': 'es-419'}) course_run_details.update({'content_language': 'es-419'})
mock_get_course_run_details.return_value = course_run_details 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) self._add_course_certificates(count=1, signatory_count=2)
...@@ -1291,6 +1346,36 @@ class CertificatesViewsTests(CommonCertificatesTestCase): ...@@ -1291,6 +1346,36 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, 'course name: test_right_lang_template') 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) @ddt.data(True, False)
@patch('certificates.views.webview.get_course_run_details') @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): 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): ...@@ -249,20 +249,29 @@ def _update_course_context(request, context, course, course_key, platform_name):
'{partner_short_name}.').format( '{partner_short_name}.').format(
partner_short_name=context['organization_short_name'], partner_short_name=context['organization_short_name'],
platform_name=platform_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'] def _update_context_with_catalog_data(context, course_key):
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'): Updates context dictionary with relevant course run info from Discovery.
# Calculate duration of the course run in weeks, multiplied by max_effort for total Hours of Effort """
try: course_certificate_settings = CertificateGenerationCourseSetting.get(course_key)
start = parser.parse(course_run_data.get('start')) if course_certificate_settings:
end = parser.parse(course_run_data.get('end')) course_run_fields = []
max_effort = int(course_run_data.get('max_effort')) if course_certificate_settings.language_specific_templates_enabled:
context['hours_of_effort'] = ((end - start).days / 7) * max_effort course_run_fields.append('content_language')
except ValueError: if course_certificate_settings.include_hours_of_effort:
log.exception('Error occurred while parsing course run details') course_run_fields.extend(['weeks_to_complete', 'max_effort'])
context['content_language'] = course_run_data.get('content_language') 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):
...@@ -592,6 +601,9 @@ def render_html_view(request, user_id, course_id): ...@@ -592,6 +601,9 @@ def render_html_view(request, user_id, course_id):
# Append course info # Append course info
_update_course_context(request, context, course, course_key, platform_name) _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 # Append user info
_update_context_with_user_info(context, user, user_certificate) _update_context_with_user_info(context, user, user_certificate)
......
...@@ -119,7 +119,8 @@ class CourseRunFactory(DictFactoryBase): ...@@ -119,7 +119,8 @@ class CourseRunFactory(DictFactoryBase):
type = 'verified' type = 'verified'
uuid = factory.Faker('uuid4') uuid = factory.Faker('uuid4')
content_language = 'en' content_language = 'en'
max_effort = 5 max_effort = 4
weeks_to_complete = 10
class CourseFactory(DictFactoryBase): class CourseFactory(DictFactoryBase):
......
...@@ -349,11 +349,10 @@ class TestGetCourseRunDetails(CatalogIntegrationMixin, TestCase): ...@@ -349,11 +349,10 @@ class TestGetCourseRunDetails(CatalogIntegrationMixin, TestCase):
course_run = CourseRunFactory() course_run = CourseRunFactory()
course_run_details = { course_run_details = {
'content_language': course_run['content_language'], 'content_language': course_run['content_language'],
'start': course_run['start'], 'weeks_to_complete': course_run['weeks_to_complete'],
'end': course_run['end'],
'max_effort': course_run['max_effort'] 'max_effort': course_run['max_effort']
} }
mock_get_edx_api_data.return_value = course_run_details 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.assertTrue(mock_get_edx_api_data.called)
self.assertEqual(data, course_run_details) self.assertEqual(data, course_run_details)
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