Merge pull request #29976 from eduNEXT/MJG/certificate-render-filter

[BD-32] feat: add filter before certificate rendering process starts
This commit is contained in:
Maria Grimaldi
2022-05-10 14:37:22 -04:00
committed by GitHub
3 changed files with 295 additions and 3 deletions

View File

@@ -0,0 +1,269 @@
"""
Test that various filters are fired for views in the certificates app.
"""
from django.conf import settings
from django.http import HttpResponse
from django.test import override_settings
from openedx_filters import PipelineStep
from openedx_filters.learning.filters import CertificateRenderStarted
from rest_framework import status
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from lms.djangoapps.certificates.models import CertificateTemplate
from lms.djangoapps.certificates.tests.test_webview_views import CommonCertificatesTestCase
from lms.djangoapps.certificates.utils import get_certificate_url
from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration
from openedx.core.djangolib.testing.utils import skip_unless_lms
FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy()
FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True
class TestStopCertificateRenderStep(PipelineStep):
"""
Utility class used when getting steps for pipeline.
"""
def run_filter(self, context, custom_template): # pylint: disable=arguments-differ
"""Pipeline step that stops the certificate render process."""
raise CertificateRenderStarted.RenderAlternativeInvalidCertificate(
"You can't generate a certificate from this site.",
)
class TestRedirectToPageStep(PipelineStep):
"""
Utility class used when getting steps for pipeline.
"""
def run_filter(self, context, custom_template): # pylint: disable=arguments-differ
"""Pipeline step that redirects to another page before rendering the certificate."""
raise CertificateRenderStarted.RedirectToPage(
"You can't generate a certificate from this site, redirecting to the correct location.",
redirect_to="https://certificate.pdf",
)
class TestRenderCustomResponse(PipelineStep):
"""
Utility class used when getting steps for pipeline.
"""
def run_filter(self, context, custom_template): # pylint: disable=arguments-differ
"""Pipeline step that returns a custom response when rendering the certificate."""
response = HttpResponse("Here's the text of the web page.")
raise CertificateRenderStarted.RenderCustomResponse(
"You can't generate a certificate from this site.",
response=response,
)
class TestCertificateRenderPipelineStep(PipelineStep):
"""
Utility class used when getting steps for pipeline.
"""
def run_filter(self, context, custom_template): # pylint: disable=arguments-differ
"""
Pipeline step that gets or creates a new custom template to render instead
of the original.
"""
custom_template = self._create_custom_template(mode='honor')
return {"custom_template": custom_template}
def _create_custom_template(self, org_id=None, mode=None, course_key=None, language=None):
"""
Creates a custom certificate template entry in DB.
"""
template_html = """
<%namespace name='static' file='static_content.html'/>
<html>
<body>
lang: ${LANGUAGE_CODE}
course name: ${accomplishment_copy_course_name}
mode: ${course_mode}
${accomplishment_copy_course_description}
${twitter_url}
<img class="custom-logo" src="test-logo.png" />
</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()
return template
@skip_unless_lms
class CertificateFiltersTest(CommonCertificatesTestCase, SharedModuleStoreTestCase):
"""
Tests for the Open edX Filters associated with the certificate rendering process.
This class guarantees that the following filters are triggered during the user's certificate rendering:
- CertificateRenderStarted
"""
@override_settings(
OPEN_EDX_FILTERS_CONFIG={
"org.openedx.learning.certificate.render.started.v1": {
"pipeline": [
"lms.djangoapps.certificates.views.tests.test_filters.TestCertificateRenderPipelineStep",
],
"fail_silently": False,
},
},
FEATURES=FEATURES_WITH_CERTS_ENABLED,
)
def test_certificate_render_filter_executed(self):
"""
Test whether the student certificate render filter is triggered before the user's
certificate rendering process.
Expected result:
- CertificateRenderStarted is triggered and executes TestCertificateRenderPipelineStep.
- The certificate renders using the custom template.
"""
test_url = get_certificate_url(
user_id=self.user.id,
course_id=str(self.course.id),
uuid=self.cert.verify_uuid
)
self._add_course_certificates(count=1, signatory_count=1, is_active=True)
response = self.client.get(test_url)
self.assertContains(
response,
'<img class="custom-logo" src="test-logo.png" />',
)
@override_settings(
OPEN_EDX_FILTERS_CONFIG={
"org.openedx.learning.certificate.render.started.v1": {
"pipeline": [
"lms.djangoapps.certificates.views.tests.test_filters.TestStopCertificateRenderStep",
],
"fail_silently": False,
},
},
FEATURES=FEATURES_WITH_CERTS_ENABLED,
)
def test_certificate_render_invalid(self):
"""
Test rendering an invalid template after catching RenderAlternativeInvalidCertificate exception.
Expected result:
- CertificateRenderStarted is triggered and executes TestStopCertificateRenderStep.
- The invalid certificate template is rendered.
"""
test_url = get_certificate_url(
user_id=self.user.id,
course_id=str(self.course.id),
uuid=self.cert.verify_uuid
)
self._add_course_certificates(count=1, signatory_count=1, is_active=True)
response = self.client.get(test_url)
self.assertContains(response, "Invalid Certificate")
self.assertContains(response, "Cannot Find Certificate")
self.assertContains(response, "We cannot find a certificate with this URL or ID number.")
@override_settings(
OPEN_EDX_FILTERS_CONFIG={
"org.openedx.learning.certificate.render.started.v1": {
"pipeline": [
"lms.djangoapps.certificates.views.tests.test_filters.TestRedirectToPageStep",
],
"fail_silently": False,
},
},
FEATURES=FEATURES_WITH_CERTS_ENABLED,
)
def test_certificate_redirect(self):
"""
Test redirecting to a new page after catching RedirectToPage exception.
Expected result:
- CertificateRenderStarted is triggered and executes TestRedirectToPageStep.
- The webview response is a redirection.
"""
test_url = get_certificate_url(
user_id=self.user.id,
course_id=str(self.course.id),
uuid=self.cert.verify_uuid
)
self._add_course_certificates(count=1, signatory_count=1, is_active=True)
response = self.client.get(test_url)
self.assertEqual(status.HTTP_302_FOUND, response.status_code)
self.assertEqual("https://certificate.pdf", response.url)
@override_settings(
OPEN_EDX_FILTERS_CONFIG={
"org.openedx.learning.certificate.render.started.v1": {
"pipeline": [
"lms.djangoapps.certificates.views.tests.test_filters.TestRenderCustomResponse",
],
"fail_silently": False,
},
},
FEATURES=FEATURES_WITH_CERTS_ENABLED,
)
def test_certificate_render_custom_response(self):
"""
Test rendering an invalid template after catching RenderCustomResponse exception.
Expected result:
- CertificateRenderStarted is triggered and executes TestRenderCustomResponse.
- The custom response is found in the certificate.
"""
test_url = get_certificate_url(
user_id=self.user.id,
course_id=str(self.course.id),
uuid=self.cert.verify_uuid
)
self._add_course_certificates(count=1, signatory_count=1, is_active=True)
response = self.client.get(test_url)
self.assertContains(response, "Here's the text of the web page.")
@override_settings(
OPEN_EDX_FILTERS_CONFIG={},
FEATURES=FEATURES_WITH_CERTS_ENABLED,
)
@with_site_configuration(
configuration={
'platform_name': 'My Platform Site',
},
)
def test_certificate_render_without_filter_config(self):
"""
Test whether the student certificate filter is triggered before the user's
certificate rendering without affecting its execution flow.
Expected result:
- CertificateRenderStarted executes a noop (empty pipeline).
- The webview response is HTTP_200_OK.
"""
test_url = get_certificate_url(
user_id=self.user.id,
course_id=str(self.course.id),
uuid=self.cert.verify_uuid
)
self._add_course_certificates(count=1, signatory_count=1, is_active=True)
response = self.client.get(test_url)
self.assertEqual(status.HTTP_200_OK, response.status_code)
self.assertContains(response, "My Platform Site")

View File

@@ -11,13 +11,14 @@ from uuid import uuid4
import pytz
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http import Http404, HttpResponse
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.template import RequestContext
from django.utils import translation
from django.utils.encoding import smart_str
from eventtracking import tracker
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from openedx_filters.learning.filters import CertificateRenderStarted
from organizations import api as organizations_api
from edx_django_utils.plugins import pluggable_override
@@ -511,7 +512,7 @@ def render_cert_by_uuid(request, certificate_uuid):
test_func=lambda request: request.GET.get('preview', None)
)
@pluggable_override('OVERRIDE_RENDER_CERTIFICATE_VIEW')
def render_html_view(request, course_id, certificate=None):
def render_html_view(request, course_id, certificate=None): # pylint: disable=too-many-statements
"""
This public view generates an HTML representation of the specified user and course
If a certificate is not available, we display a "Sorry!" screen instead
@@ -642,8 +643,30 @@ def render_html_view(request, course_id, certificate=None):
# Track certificate view events
_track_certificate_events(request, course, user, user_certificate)
try:
# .. filter_implemented_name: CertificateRenderStarted
# .. filter_type: org.openedx.learning.certificate.render.started.v1
context, custom_template = CertificateRenderStarted.run_filter(
context=context,
custom_template=custom_template,
)
except CertificateRenderStarted.RenderAlternativeInvalidCertificate as exc:
response = _render_invalid_certificate(
request,
course_id,
platform_name,
configuration,
cert_path=exc.template_name or INVALID_CERTIFICATE_TEMPLATE_PATH,
)
except CertificateRenderStarted.RedirectToPage as exc:
response = HttpResponseRedirect(exc.redirect_to)
except CertificateRenderStarted.RenderCustomResponse as exc:
response = exc.response
else:
response = _render_valid_certificate(request, context, custom_template)
# Render the certificate
return _render_valid_certificate(request, context, custom_template)
return response
def _get_catalog_data_for_course(course_key):