diff --git a/lms/djangoapps/certificates/views/tests/__init__.py b/lms/djangoapps/certificates/views/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/certificates/views/tests/test_filters.py b/lms/djangoapps/certificates/views/tests/test_filters.py new file mode 100644 index 0000000000..90b105f8ce --- /dev/null +++ b/lms/djangoapps/certificates/views/tests/test_filters.py @@ -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'/> + +
+ lang: ${LANGUAGE_CODE} + course name: ${accomplishment_copy_course_name} + mode: ${course_mode} + ${accomplishment_copy_course_description} + ${twitter_url} +
+
+
+ """
+ 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,
+ '
',
+ )
+
+ @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")
diff --git a/lms/djangoapps/certificates/views/webview.py b/lms/djangoapps/certificates/views/webview.py
index 4209b43a41..b859cfe92c 100644
--- a/lms/djangoapps/certificates/views/webview.py
+++ b/lms/djangoapps/certificates/views/webview.py
@@ -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):