Commit 0375e42d by McKenzie Welter Committed by GitHub

Merge pull request #16167 from edx/McKenzieW/course-cert-hours-of-effort

Flag for Hours of Effort in course certificates
parents d64e0b95 39bf59e6
......@@ -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
......
# -*- 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):
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 is_self_generation_enabled_for_course(cls, course_key):
"""Check whether self-generated certificates are enabled for a course.
def get(cls, course_key):
""" Retrieve certificate generation settings for a course.
Arguments:
course_key (CourseKey): The identifier for the course.
Returns:
boolean
CertificateGenerationCourseSetting
"""
try:
latest = cls.objects.filter(course_key=course_key).latest()
except cls.DoesNotExist:
return False
return None
else:
return latest.self_generation_enabled
return latest
@classmethod
def set_self_generatation_enabled_for_course(cls, course_key, is_enabled):
"""Enable or disable self-generated certificates 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.
def is_self_generation_enabled_for_course(cls, course_key):
"""Check whether self-generated certificates are enabled for a course.
Arguments:
course_key (CourseKey): The identifier for the course.
......@@ -926,19 +917,19 @@ class CertificateGenerationCourseSetting(TimeStampedModel):
except cls.DoesNotExist:
return False
else:
return latest.language_specific_templates_enabled
return latest.self_generation_enabled
@classmethod
def set_language_specific_templates_enabled_for_course(cls, course_key, is_enabled):
"""Enable or disable language-specific certificates for a course.
def set_self_generatation_enabled_for_course(cls, course_key, is_enabled):
"""Enable or disable self-generated certificates for a course.
Arguments:
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 = {
'language_specific_templates_enabled': is_enabled,
'self_generation_enabled': is_enabled
}
CertificateGenerationCourseSetting.objects.update_or_create(
course_key=course_key,
......
......@@ -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)
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