Commit 2467d697 by Zia Fazal Committed by Matt Drayer

ziafazal/SOL-1044: Database-Driven Certificate Templates

* added certificate template model
* added certificate template asset model
* added django admin configs and migration
* initial LMS template matching logic
* improved LMS template matching
* improved LMS template matching and rendering logic
* address Django admin form/model load order incongruence
* add missing unique constraint migration.
parent 00eb18a1
......@@ -4,6 +4,7 @@ Utility library for working with the edx-organizations app
"""
from django.conf import settings
from django.db.utils import DatabaseError
def add_organization(organization_data):
......@@ -43,7 +44,15 @@ def get_organizations():
if not settings.FEATURES.get('ORGANIZATIONS_APP', False):
return []
from organizations import api as organizations_api
return organizations_api.get_organizations()
# Due to the way unit tests run for edx-platform, models are not yet available at the time
# of Django admin form instantiation. This unfortunately results in an invocation of the following
# workflow, because the test configuration is (correctly) configured to exercise the application
# The good news is that this case does not manifest in the Real World, because migrations have
# been run ahead of application instantiation and the flag set only when that is truly the case.
try:
return organizations_api.get_organizations()
except DatabaseError:
return []
def get_organization_courses(organization_id):
......
......@@ -2,12 +2,49 @@
django admin pages for certificates models
"""
from django.contrib import admin
from django import forms
from config_models.admin import ConfigurationModelAdmin
from util.organizations_helpers import get_organizations
from certificates.models import (
CertificateGenerationConfiguration, CertificateHtmlViewConfiguration, BadgeImageConfiguration
CertificateGenerationConfiguration,
CertificateHtmlViewConfiguration,
BadgeImageConfiguration,
CertificateTemplate,
CertificateTemplateAsset,
)
class CertificateTemplateForm(forms.ModelForm):
"""
Django admin form for CertificateTemplate model
"""
organizations = get_organizations()
org_choices = [(org["id"], org["name"]) for org in organizations]
org_choices.insert(0, ('', 'None'))
organization_id = forms.TypedChoiceField(choices=org_choices, required=False, coerce=int, empty_value=None)
class Meta(object):
""" Meta definitions for CertificateTemplateForm """
model = CertificateTemplate
class CertificateTemplateAdmin(admin.ModelAdmin):
"""
Django admin customizations for CertificateTemplate model
"""
list_display = ('name', 'description', 'organization_id', 'course_key', 'mode', 'is_active')
form = CertificateTemplateForm
class CertificateTemplateAssetAdmin(admin.ModelAdmin):
"""
Django admin customizations for CertificateTemplateAsset model
"""
list_display = ('description', '__unicode__')
admin.site.register(CertificateGenerationConfiguration)
admin.site.register(CertificateHtmlViewConfiguration, ConfigurationModelAdmin)
admin.site.register(BadgeImageConfiguration)
admin.site.register(CertificateTemplate, CertificateTemplateAdmin)
admin.site.register(CertificateTemplateAsset, CertificateTemplateAssetAdmin)
......@@ -14,6 +14,7 @@ from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from xmodule.modulestore.django import modulestore
from util.organizations_helpers import get_course_organizations
from certificates.models import (
CertificateStatuses,
......@@ -21,7 +22,8 @@ from certificates.models import (
CertificateGenerationCourseSetting,
CertificateGenerationConfiguration,
ExampleCertificateSet,
GeneratedCertificate
GeneratedCertificate,
CertificateTemplate,
)
from certificates.queue import XQueueCertInterface
......@@ -373,6 +375,46 @@ def get_active_web_certificate(course, is_preview_mode=None):
return None
def get_certificate_template(course_key, mode):
"""
Retrieves the custom certificate template based on course_key and mode.
"""
org_id, template = None, None
# fetch organization of the course
course_organization = get_course_organizations(course_key)
if course_organization:
org_id = course_organization[0]['id']
if org_id and mode:
template = CertificateTemplate.objects.filter(
organization_id=org_id,
course_key=course_key,
mode=mode,
is_active=True
)
# if don't template find by org and mode
if not template and org_id and mode:
template = CertificateTemplate.objects.filter(
organization_id=org_id,
mode=mode,
is_active=True
)
# if don't template find by only org
if not template and org_id:
template = CertificateTemplate.objects.filter(
organization_id=org_id,
is_active=True
)
# if we still don't template find by only course mode
if not template and mode:
template = CertificateTemplate.objects.filter(
mode=mode,
is_active=True
)
return template[0].template if template else None
def emit_certificate_event(event_name, user, course_id, course=None, event_data=None):
"""
Emits certificate event.
......
......@@ -674,6 +674,88 @@ class BadgeImageConfiguration(models.Model):
return cls.objects.get(default=True).icon
class CertificateTemplate(TimeStampedModel):
"""A set of custom web certificate templates.
Web certificate templates are Django web templates
to replace PDF certificate.
A particular course may have several kinds of certificate templates
(e.g. honor and verified).
"""
name = models.CharField(
max_length=255,
help_text=_(u'Name of template.'),
)
description = models.CharField(
max_length=255,
null=True,
blank=True,
help_text=_(u'Description and/or admin notes.'),
)
template = models.TextField(
help_text=_(u'Django template HTML.'),
)
organization_id = models.IntegerField(
null=True,
blank=True,
db_index=True,
help_text=_(u'Organization of template.'),
)
course_key = CourseKeyField(
max_length=255,
null=True,
blank=True,
db_index=True,
)
mode = models.CharField(
max_length=125,
choices=GeneratedCertificate.MODES,
default=GeneratedCertificate.MODES.honor,
null=True,
blank=True,
help_text=_(u'The course mode for this template.'),
)
is_active = models.BooleanField(
help_text=_(u'On/Off switch.'),
default=False,
)
def __unicode__(self):
return u'%s' % (self.name, )
class Meta(object): # pylint: disable=missing-docstring
get_latest_by = 'created'
unique_together = (('organization_id', 'course_key', 'mode'),)
class CertificateTemplateAsset(TimeStampedModel):
"""A set of assets to be used in custom web certificate templates.
This model stores assets used in custom web certificate templates
such as image, css files.
"""
description = models.CharField(
max_length=255,
null=True,
blank=True,
help_text=_(u'Description of the asset.'),
)
asset = models.FileField(
max_length=255,
upload_to='certificate_template_assets',
help_text=_(u'Asset file. It could be an image or css file.'),
)
def __unicode__(self):
return u'%s' % (self.asset.url, ) # pylint: disable=no-member
class Meta(object): # pylint: disable=missing-docstring
get_latest_by = 'created'
@receiver(post_save, sender=GeneratedCertificate)
#pylint: disable=unused-argument
def create_badge(sender, instance, **kwargs):
......
......@@ -30,6 +30,7 @@ from certificates.models import (
CertificateStatuses,
CertificateHtmlViewConfiguration,
CertificateSocialNetworks,
CertificateTemplate,
)
from certificates.tests.factories import (
......@@ -45,6 +46,11 @@ FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True
FEATURES_WITH_CERTS_DISABLED = settings.FEATURES.copy()
FEATURES_WITH_CERTS_DISABLED['CERTIFICATES_HTML_VIEW'] = False
FEATURES_WITH_CUSTOM_CERTS_ENABLED = {
"CUSTOM_CERTIFICATE_TEMPLATES_ENABLED": True
}
FEATURES_WITH_CUSTOM_CERTS_ENABLED.update(FEATURES_WITH_CERTS_ENABLED)
@attr('shard_1')
@ddt.ddt
......@@ -427,6 +433,30 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
self.course.save()
self.store.update_item(self.course, self.user.id)
def _create_custom_template(self, org_id=None, mode=None, course_key=None):
"""
Creates a custom certificate template entry in DB.
"""
template_html = """
<html>
<body>
lang: ${LANGUAGE_CODE}
course name: ${accomplishment_copy_course_name}
mode: ${course_mode}
${accomplishment_copy_course_description}
</body>
</html>
"""
template = CertificateTemplate(
name='custom template',
template=template_html,
organization_id=org_id,
course_key=course_key,
mode=mode,
is_active=True
)
template.save()
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
def test_rendering_course_organization_data(self):
"""
......@@ -724,6 +754,75 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
response_json = json.loads(response.content)
self.assertEqual(CertificateStatuses.generating, response_json['add_status'])
@override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED)
@override_settings(LANGUAGE_CODE='fr')
def test_certificate_custom_template_with_org_mode_course(self):
"""
Tests custom template search and rendering.
"""
self._add_course_certificates(count=1, signatory_count=2)
self._create_custom_template(1, mode='honor', course_key=unicode(self.course.id))
self._create_custom_template(2, mode='honor')
test_url = get_certificate_url(
user_id=self.user.id,
course_id=unicode(self.course.id)
)
with patch('certificates.api.get_course_organizations') as mock_get_orgs:
mock_get_orgs.side_effect = [
[{"id": 1, "name": "organization name"}],
[{"id": 2, "name": "organization name 2"}],
]
response = self.client.get(test_url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'lang: fr')
self.assertContains(response, 'course name: {}'.format(self.course.display_name))
# test with second organization template
response = self.client.get(test_url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'lang: fr')
self.assertContains(response, 'course name: {}'.format(self.course.display_name))
@override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED)
def test_certificate_custom_template_with_org(self):
"""
Tests custom template search if if have a single template for all courses of organization.
"""
self._add_course_certificates(count=1, signatory_count=2)
self._create_custom_template(1)
self._create_custom_template(1, mode='honor')
test_url = get_certificate_url(
user_id=self.user.id,
course_id=unicode(self.course.id)
)
with patch('certificates.api.get_course_organizations') as mock_get_orgs:
mock_get_orgs.side_effect = [
[{"id": 1, "name": "organization name"}],
]
response = self.client.get(test_url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'course name: {}'.format(self.course.display_name))
@override_settings(FEATURES=FEATURES_WITH_CUSTOM_CERTS_ENABLED)
def test_certificate_custom_template_with_course_mode(self):
"""
Tests custom template search if if have a single template for a course mode.
"""
mode = 'honor'
self._add_course_certificates(count=1, signatory_count=2)
self._create_custom_template(mode=mode)
test_url = get_certificate_url(
user_id=self.user.id,
course_id=unicode(self.course.id)
)
with patch('certificates.api.get_course_organizations') as mock_get_orgs:
mock_get_orgs.return_value = []
response = self.client.get(test_url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'mode: {}'.format(mode))
class TrackShareRedirectTest(UrlResetMixin, ModuleStoreTestCase, EventTrackingTestCase):
"""
......
......@@ -7,22 +7,27 @@ import logging
from django.conf import settings
from django.contrib.auth.models import User
from django.http import HttpResponse
from django.template import RequestContext
from django.utils.translation import ugettext as _
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from microsite_configuration import microsite
from courseware.courses import course_image_url
from edxmako.shortcuts import render_to_response
from edxmako.template import Template
from eventtracking import tracker
from xmodule.modulestore.django import modulestore
from microsite_configuration import microsite
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from student.models import LinkedInAddToProfileConfiguration
from courseware.courses import course_image_url
from util import organizations_helpers as organization_api
from xmodule.modulestore.django import modulestore
from certificates.api import (
get_active_web_certificate,
get_certificate_url,
emit_certificate_event,
has_html_certificates_enabled
has_html_certificates_enabled,
get_certificate_template
)
from certificates.models import (
GeneratedCertificate,
......@@ -31,7 +36,6 @@ from certificates.models import (
BadgeAssertion
)
log = logging.getLogger(__name__)
......@@ -401,4 +405,11 @@ def render_html_view(request, user_id, course_id):
context.update(course.cert_html_view_overrides)
# FINALLY, generate and send the output the client
if settings.FEATURES.get('CUSTOM_CERTIFICATE_TEMPLATES_ENABLED', False):
custom_template = get_certificate_template(course_key, user_certificate.mode)
if custom_template:
template = Template(custom_template)
context = RequestContext(request, context)
return HttpResponse(template.render(context))
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