"""Tests for certificates views. """ import datetime from unittest import skipUnless from unittest.mock import patch from urllib.parse import urlencode from uuid import uuid4 import ddt from django.conf import settings from django.test.client import Client, RequestFactory from django.test.utils import override_settings from django.urls import reverse from edx_toggles.toggles.testutils import override_waffle_switch from organizations import api as organizations_api from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.roles import CourseStaffRole from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory from common.djangoapps.track.tests import EventTrackingTestCase from common.djangoapps.util.date_utils import strftime_localized from lms.djangoapps.certificates.config import AUTO_CERTIFICATE_GENERATION from lms.djangoapps.certificates.models import ( CertificateGenerationCourseSetting, CertificateSocialNetworks, CertificateStatuses, CertificateTemplate, CertificateTemplateAsset, GeneratedCertificate ) from lms.djangoapps.certificates.tests.factories import ( CertificateDateOverrideFactory, CertificateHtmlViewConfigurationFactory, GeneratedCertificateFactory, LinkedInAddToProfileConfigurationFactory ) from lms.djangoapps.certificates.utils import get_certificate_url from openedx.core.djangoapps.dark_lang.models import DarkLangConfig from openedx.core.djangoapps.site_configuration.tests.test_util import ( with_site_configuration, with_site_configuration_context ) from openedx.core.djangolib.js_utils import js_escaped_string from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from openedx.core.lib.courses import course_image_url from openedx.core.lib.tests.assertions.events import assert_event_matches from openedx.features.name_affirmation_api.utils import get_name_affirmation_service from xmodule.data import CertificatesDisplayBehaviors # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy() 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 = FEATURES_WITH_CERTS_ENABLED.copy() FEATURES_WITH_CUSTOM_CERTS_ENABLED['CUSTOM_CERTIFICATE_TEMPLATES_ENABLED'] = True name_affirmation_service = get_name_affirmation_service() class CommonCertificatesTestCase(ModuleStoreTestCase): """ Common setUp and utility methods for Certificate tests """ ENABLED_SIGNALS = ['course_published'] def setUp(self): super().setUp() self.client = Client() self.course = CourseFactory.create( org='testorg', number='run1', display_name='refundable course', certificate_available_date=datetime.datetime.today() - datetime.timedelta(days=1), certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE ) self.course_id = self.course.location.course_key self.user = UserFactory.create( email='joe_user@edx.org', username='joeuser', password='foo' ) self.user.profile.name = "Joe User" self.user.profile.save() self.client.login(username=self.user.username, password='foo') self.request = RequestFactory().request() self.linkedin_url = 'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&{params}' self.cert = GeneratedCertificateFactory.create( user=self.user, course_id=self.course_id, download_uuid=uuid4().hex, download_url="https://www.example.com/certificates/download", grade="0.95", key='the_key', distinction=True, status=CertificateStatuses.downloadable, mode='honor', name=self.user.profile.name, ) CourseEnrollmentFactory.create( user=self.user, course_id=self.course_id, mode=CourseMode.HONOR, ) CertificateHtmlViewConfigurationFactory.create() LinkedInAddToProfileConfigurationFactory.create() def _add_course_certificates(self, count=1, signatory_count=0, is_active=True): """ Create certificate for the course. """ signatories = [ { 'name': 'Signatory_Name ' + str(i), 'title': 'Signatory_Title ' + str(i), 'organization': 'Signatory_Organization ' + str(i), 'signature_image_path': f'/static/certificates/images/demo-sig{i}.png', 'id': i } for i in range(signatory_count) ] certificates = [ { 'id': i, 'name': 'Name ' + str(i), 'description': 'Description ' + str(i), 'course_title': 'course_title_' + str(i), 'org_logo_path': f'/t4x/orgX/testX/asset/org-logo-{i}.png', 'signatories': signatories, 'version': 1, 'is_active': is_active } for i in range(count) ] self.course.certificates = {'certificates': certificates} self.course.cert_html_view_enabled = True self.course.save() self.update_course(self.course, self.user.id) 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() def _create_custom_named_template(self, template_name, 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: """ + template_name + """ mode: ${course_mode} ${accomplishment_copy_course_description} ${twitter_url} """ template = CertificateTemplate( name=template_name, template=template_html, organization_id=org_id, course_key=course_key, mode=mode, is_active=True, language=language ) 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'/> 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} """ 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() def _create_custom_template_with_verified_description(self, org_id=None, course_key=None, language=None): """ Creates a custom certificate template entry in DB. This custom certificate can be used to test that the correct language is used if the IDV requirement on certificates has been enabled for a course. """ template_html = """ <%namespace name='static' file='static_content.html'/> lang: ${LANGUAGE_CODE} course name: ${accomplishment_copy_course_name} mode: verified ${accomplishment_copy_course_description} ${certificate_type_description} % if not idv_enabled_for_certificates:

IDV disabled

%endif ${twitter_url} """ template = CertificateTemplate( name='custom template', template=template_html, organization_id=org_id, course_key=course_key, mode='verified', is_active=True, language=language ) template.save() def _add_certificate_date_override(self): """ Creates a mock CertificateDateOverride and adds it to the certificate """ self.cert.date_override = CertificateDateOverrideFactory.create( generated_certificate=self.cert, overridden_by=self.user, ) @ddt.ddt class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase): """ Tests for the certificates web/html views """ def setUp(self): super().setUp() self.mock_course_run_details = { 'content_language': 'en', 'weeks_to_complete': '4', 'max_effort': '10' } @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_linkedin_share_url(self): """ Test: LinkedIn share URL. """ self._add_course_certificates(count=1, signatory_count=1, is_active=True) test_url = get_certificate_url(course_id=self.course.id, uuid=self.cert.verify_uuid) response = self.client.get(test_url) assert response.status_code == 200 params = { 'name': '{platform_name} Honor Code Certificate for {course_name}'.format( platform_name=settings.PLATFORM_NAME, course_name=self.course.display_name, ).encode('utf-8'), 'certUrl': self.request.build_absolute_uri(test_url), # default value from the LinkedInAddToProfileConfigurationFactory company_identifier 'organizationId': 1337, 'certId': self.cert.verify_uuid, 'issueYear': self.cert.created_date.year, 'issueMonth': self.cert.created_date.month, } self.assertContains( response, js_escaped_string(self.linkedin_url.format(params=urlencode(params))), ) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) @with_site_configuration( configuration={ 'platform_name': 'My Platform Site', 'LINKEDIN_COMPANY_ID': 2448, }, ) def test_linkedin_share_url_site(self): """ Test: LinkedIn share URL should be visible when called from within a site. """ self._add_course_certificates(count=1, signatory_count=1, is_active=True) test_url = get_certificate_url(course_id=self.cert.course_id, uuid=self.cert.verify_uuid) response = self.client.get(test_url, HTTP_HOST='test.localhost') assert response.status_code == 200 # the linkedIn share URL with appropriate parameters should be present params = { 'name': 'My Platform Site Honor Code Certificate for {course_name}'.format( course_name=self.course.display_name, ).encode('utf-8'), 'certUrl': 'http://test.localhost' + test_url, 'organizationId': 2448, 'certId': self.cert.verify_uuid, 'issueYear': self.cert.created_date.year, 'issueMonth': self.cert.created_date.month, } self.assertContains( response, js_escaped_string(self.linkedin_url.format(params=urlencode(params))), ) @patch.dict("django.conf.settings.SOCIAL_SHARING_SETTINGS", { "CERTIFICATE_FACEBOOK": True, "CERTIFICATE_FACEBOOK_TEXT": "test FB text" }) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_render_certificate_html_view_with_facebook_meta_tags(self): """ Test view html certificate if share to FB is enabled. If 'facebook_share_enabled=True', tags with property="og:..." must be enabled to pass parameters to FB. """ self._add_course_certificates(count=1, signatory_count=1, is_active=True) self.course.cert_html_view_enabled = True self.course.save() self.update_course(self.course, self.user.id) test_url = get_certificate_url( user_id=self.user.id, course_id=str(self.course.id), uuid=self.cert.verify_uuid ) platform_name = settings.PLATFORM_NAME share_url = f'http://testserver{test_url}' full_course_image_url = f'http://testserver{course_image_url(self.course)}' document_title = f'{self.course.org} {self.course.number} Certificate | {platform_name}' response = self.client.get(test_url) assert response.status_code == 200 self.assertContains(response, f'') self.assertContains(response, f'') self.assertContains(response, '') self.assertContains(response, f'') self.assertContains(response, '') @patch.dict("django.conf.settings.SOCIAL_SHARING_SETTINGS", { "CERTIFICATE_FACEBOOK": False, }) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) def test_render_certificate_html_view_without_facebook_meta_tags(self): """ Test view html certificate if share to FB is disabled. If 'facebook_share_enabled=False', html certificate view should not contain tags with parameters property="og:..." """ self._add_course_certificates(count=1, signatory_count=1, is_active=True) self.course.cert_html_view_enabled = True self.course.save() self.update_course(self.course, self.user.id) test_url = get_certificate_url( user_id=self.user.id, course_id=str(self.course.id), uuid=self.cert.verify_uuid ) response = self.client.get(test_url) assert response.status_code == 200 self.assertNotContains(response, '') self.assertNotContains(response, 'test_organization {self.course.number} Certificate |') self.assertContains(response, 'logo_test1.png') @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) @patch.dict("django.conf.settings.SOCIAL_SHARING_SETTINGS", { "CERTIFICATE_TWITTER": True, "CERTIFICATE_FACEBOOK": True, }) @with_site_configuration( configuration=dict( platform_name='My Platform Site', SITE_NAME='test_site.localhost', urls=dict( ABOUT='https://www.test-site.org/about-us', ), ), ) def test_rendering_maximum_data(self): """ Tests at least one data item from different context update methods to make sure every context update method is invoked while rendering certificate template. """ long_org_name = 'Long org name' short_org_name = 'short_org_name' test_organization_data = { 'name': long_org_name, 'short_name': short_org_name, 'description': 'Test Organization Description', 'active': True, 'logo': '/logo_test1.png' } test_org = organizations_api.add_organization(organization_data=test_organization_data) organizations_api.add_organization_course(organization_data=test_org, course_key=str(self.course.id)) self._add_course_certificates(count=1, signatory_count=1, is_active=True) self.course.cert_html_view_overrides = { "logo_src": "/static/certificates/images/course_override_logo.png" } self.course.save() self.update_course(self.course, self.user.id) test_url = get_certificate_url( user_id=self.user.id, course_id=str(self.course.id), uuid=self.cert.verify_uuid ) response = self.client.get(test_url, HTTP_HOST='test.localhost') # Test an item from basic info self.assertContains(response, 'Terms of Service & Honor Code') self.assertContains(response, 'Certificate ID Number') # Test an item from html cert configuration self.assertContains(response, '