diff --git a/lms/djangoapps/certificates/generation.py b/lms/djangoapps/certificates/generation.py index 9a0b944f8a41544ad303fc21fd9fc0be8ac8eca1..a6485c57ab9e852a3d7c65ba4e1ed2e4a76a6742 100644 --- a/lms/djangoapps/certificates/generation.py +++ b/lms/djangoapps/certificates/generation.py @@ -10,10 +10,9 @@ These methods should be called from tasks. import logging from uuid import uuid4 -from common.djangoapps.student import models_api as student_api from lms.djangoapps.certificates.data import CertificateStatuses from lms.djangoapps.certificates.models import GeneratedCertificate -from lms.djangoapps.certificates.utils import emit_certificate_event +from lms.djangoapps.certificates.utils import emit_certificate_event, get_preferred_certificate_name log = logging.getLogger(__name__) @@ -66,9 +65,7 @@ def _generate_certificate(user, course_key, status, enrollment_mode, course_grad # Retrieve the existing certificate for the learner if it exists existing_certificate = GeneratedCertificate.certificate_for_student(user, course_key) - profile_name = student_api.get_name(user.id) - if not profile_name: - profile_name = '' + preferred_name = get_preferred_certificate_name(user) # Retain the `verify_uuid` from an existing certificate if possible, this will make it possible for the learner to # keep the existing URL to their certificate @@ -84,7 +81,7 @@ def _generate_certificate(user, course_key, status, enrollment_mode, course_grad 'user': user, 'course_id': course_key, 'mode': enrollment_mode, - 'name': profile_name, + 'name': preferred_name, 'status': status, 'grade': course_grade, 'download_url': '', diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index 005f4c66e395d66b6669f8eb1324955b831fb440..e76849f35a3656e36f247c0ec9091497be1c14d1 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -18,6 +18,8 @@ from django.db.models import Count from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ +from edx_name_affirmation.api import get_verified_name, should_use_verified_name_for_certs +from edx_name_affirmation.toggles import is_verified_name_enabled from model_utils import Choices from model_utils.models import TimeStampedModel from opaque_keys.edx.django.models import CourseKeyField @@ -370,9 +372,7 @@ class GeneratedCertificate(models.Model): if not mode: mode = self.mode - profile_name = student_api.get_name(self.user.id) - if not profile_name: - profile_name = '' + preferred_name = self._get_preferred_certificate_name(self.user) self.error_reason = '' self.download_uuid = '' @@ -380,7 +380,7 @@ class GeneratedCertificate(models.Model): self.grade = grade self.status = status self.mode = mode - self.name = profile_name + self.name = preferred_name self.save() COURSE_CERT_REVOKED.send_robust( @@ -404,6 +404,23 @@ class GeneratedCertificate(models.Model): } emit_certificate_event('revoked', self.user, str(self.course_id), event_data=event_data) + def _get_preferred_certificate_name(self, user): + """ + Copy of `get_preferred_certificate_name` from utils.py - importing it here would introduce + a circular dependency. + """ + name_to_use = student_api.get_name(user.id) + + if is_verified_name_enabled() and should_use_verified_name_for_certs(user): + verified_name_obj = get_verified_name(user, is_verified=True) + if verified_name_obj: + name_to_use = verified_name_obj.verified_name + + if not name_to_use: + name_to_use = '' + + return name_to_use + def is_valid(self): """ Return True if certificate is valid else return False. diff --git a/lms/djangoapps/certificates/tests/test_generation.py b/lms/djangoapps/certificates/tests/test_generation.py index e7e752fd502e58108620b96bac2f55c5d126804c..add17cdae77eddf4fa655d56ac3ceafdb39a9f5e 100644 --- a/lms/djangoapps/certificates/tests/test_generation.py +++ b/lms/djangoapps/certificates/tests/test_generation.py @@ -1,9 +1,14 @@ """ Tests for certificate generation """ +import ddt import logging from unittest import mock +from edx_name_affirmation.api import create_verified_name, create_verified_name_config +from edx_name_affirmation.toggles import VERIFIED_NAME_FLAG +from edx_toggles.toggles.testutils import override_waffle_flag + from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.models import UserProfile from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory @@ -20,6 +25,7 @@ log = logging.getLogger(__name__) PROFILE_NAME_METHOD = 'common.djangoapps.student.models_api.get_name' +@ddt.ddt class CertificateTests(EventTestMixin, ModuleStoreTestCase): """ Tests for certificate generation @@ -187,3 +193,34 @@ class CertificateTests(EventTestMixin, ModuleStoreTestCase): assert cert.mode == self.enrollment_mode assert cert.grade == self.grade assert cert.name == '' + + @override_waffle_flag(VERIFIED_NAME_FLAG, active=True) + @ddt.data((True, True), (True, False), (False, False)) + @ddt.unpack + def test_generation_verified_name(self, should_use_verified_name_for_certs, is_verified): + """ + Test that if verified name functionality is enabled and the user has their preference set to use + verified name for certificates, their verified name will appear on the certificate rather than + their profile name. + """ + verified_name = 'Jonathan Doe' + create_verified_name(self.u, verified_name, self.name, is_verified=is_verified) + create_verified_name_config(self.u, use_verified_name_for_certs=should_use_verified_name_for_certs) + + GeneratedCertificateFactory( + user=self.u, + course_id=self.key, + mode=CourseMode.AUDIT, + status=CertificateStatuses.unverified + ) + + generate_course_certificate( + self.u, self.key, CertificateStatuses.downloadable, self.enrollment_mode, self.grade, self.gen_mode, + ) + + cert = GeneratedCertificate.objects.get(user=self.u, course_id=self.key) + + if should_use_verified_name_for_certs and is_verified: + assert cert.name == verified_name + else: + assert cert.name == self.name diff --git a/lms/djangoapps/certificates/tests/test_models.py b/lms/djangoapps/certificates/tests/test_models.py index 456c471cb535d51004f2566c66275b80871cdef7..2025c27289d0d909ed6969b66932f5bb1feee8e4 100644 --- a/lms/djangoapps/certificates/tests/test_models.py +++ b/lms/djangoapps/certificates/tests/test_models.py @@ -12,6 +12,9 @@ from django.core.exceptions import ValidationError from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase from django.test.utils import override_settings +from edx_name_affirmation.api import create_verified_name, create_verified_name_config +from edx_name_affirmation.toggles import VERIFIED_NAME_FLAG +from edx_toggles.toggles.testutils import override_waffle_flag from opaque_keys.edx.locator import CourseKey, CourseLocator from path import Path as path @@ -360,6 +363,7 @@ class CertificateInvalidationTest(SharedModuleStoreTestCase): assert mock_revoke_task.call_args[0] == (self.user.username, str(self.course_id)) +@ddt.ddt class GeneratedCertificateTest(SharedModuleStoreTestCase): """ Test GeneratedCertificates @@ -540,6 +544,35 @@ class GeneratedCertificateTest(SharedModuleStoreTestCase): self._assert_event_data(mock_emit_certificate_event, expected_event_data) + @override_waffle_flag(VERIFIED_NAME_FLAG, active=True) + @ddt.data((True, True), (True, False), (False, False)) + @ddt.unpack + def test_invalidate_with_verified_name(self, should_use_verified_name_for_certs, is_verified): + """ + Test the invalidate method with verified name turned on for the user's certificates + """ + verified_name = 'Jonathan Doe' + profile = UserProfile.objects.get(user=self.user) + create_verified_name(self.user, verified_name, profile.name, is_verified=is_verified) + create_verified_name_config(self.user, use_verified_name_for_certs=should_use_verified_name_for_certs) + + cert = GeneratedCertificateFactory.create( + status=CertificateStatuses.downloadable, + user=self.user, + course_id=self.course_key, + mode=CourseMode.AUDIT, + name='Fuzzy Hippo' + ) + mode = CourseMode.VERIFIED + source = 'invalidated_test' + cert.invalidate(mode=mode, source=source) + + cert = GeneratedCertificate.objects.get(user=self.user, course_id=self.course_key) + if should_use_verified_name_for_certs and is_verified: + assert cert.name == verified_name + else: + assert cert.name == profile.name + @patch('lms.djangoapps.certificates.utils.emit_certificate_event') def test_unverified(self, mock_emit_certificate_event): """ diff --git a/lms/djangoapps/certificates/tests/test_webview_views.py b/lms/djangoapps/certificates/tests/test_webview_views.py index a389e297a99f44166b0ec7e51cf5e544467d8b25..188e4b737be20e394d483eca691553de96d6c107 100644 --- a/lms/djangoapps/certificates/tests/test_webview_views.py +++ b/lms/djangoapps/certificates/tests/test_webview_views.py @@ -12,8 +12,10 @@ 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_name_affirmation.api import create_verified_name, create_verified_name_config +from edx_name_affirmation.toggles import VERIFIED_NAME_FLAG from edx_toggles.toggles import LegacyWaffleSwitch -from edx_toggles.toggles.testutils import override_waffle_switch +from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch from organizations import api as organizations_api from common.djangoapps.course_modes.models import CourseMode @@ -1524,6 +1526,35 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) ) ) + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) + @override_waffle_flag(VERIFIED_NAME_FLAG, active=True) + @ddt.data((True, True), (True, False), (False, False)) + @ddt.unpack + def test_certificate_view_verified_name(self, should_use_verified_name_for_certs, is_verified): + """ + Test that if verified name functionality is enabled and the user has their preference set to use + verified name for certificates, their verified name will appear on the certificate rather than + their profile name. + """ + verified_name = 'Jonathan Doe' + create_verified_name(self.user, verified_name, self.user.profile.name, is_verified=is_verified) + create_verified_name_config(self.user, use_verified_name_for_certs=should_use_verified_name_for_certs) + + self._add_course_certificates(count=1, signatory_count=1) + 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') + if should_use_verified_name_for_certs and is_verified: + self.assertContains(response, verified_name) + self.assertNotContains(response, self.user.profile.name) + else: + self.assertContains(response, self.user.profile.name) + self.assertNotContains(response, verified_name) + class CertificateEventTests(CommonCertificatesTestCase, EventTrackingTestCase): """ diff --git a/lms/djangoapps/certificates/utils.py b/lms/djangoapps/certificates/utils.py index da3fd0db450fc41fbf4ace8c9830f216b3f318b2..d4111436589ca31a43e5ce84e0cdbba9d213c462 100644 --- a/lms/djangoapps/certificates/utils.py +++ b/lms/djangoapps/certificates/utils.py @@ -4,12 +4,16 @@ Certificates utilities from datetime import datetime import logging +from edx_name_affirmation.api import get_verified_name, should_use_verified_name_for_certs +from edx_name_affirmation.toggles import is_verified_name_enabled + from django.conf import settings from django.urls import reverse from eventtracking import tracker from opaque_keys.edx.keys import CourseKey from pytz import utc +from common.djangoapps.student import models_api as student_api from lms.djangoapps.certificates.data import CertificateStatuses from lms.djangoapps.certificates.models import GeneratedCertificate from openedx.core.djangoapps.content.course_overviews.api import get_course_overview_or_none @@ -228,3 +232,22 @@ def certificate_status_for_student(student, course_id): except GeneratedCertificate.DoesNotExist: generated_certificate = None return certificate_status(generated_certificate) + + +def get_preferred_certificate_name(user): + """ + If the verified name feature is enabled and the user has their preference set to use their + verified name for certificates, return their verified name. Else, return the user's profile + name, or an empty string if it doesn't exist. + """ + name_to_use = student_api.get_name(user.id) + + if is_verified_name_enabled() and should_use_verified_name_for_certs(user): + verified_name_obj = get_verified_name(user, is_verified=True) + if verified_name_obj: + name_to_use = verified_name_obj.verified_name + + if not name_to_use: + name_to_use = '' + + return name_to_use diff --git a/lms/djangoapps/certificates/views/webview.py b/lms/djangoapps/certificates/views/webview.py index f37b3c64e45eb0fa1d6f2d8d40149bf973a8e246..e18137eacde31d7d6e204c052fc99ee0efdfb9ab 100644 --- a/lms/djangoapps/certificates/views/webview.py +++ b/lms/djangoapps/certificates/views/webview.py @@ -43,7 +43,11 @@ from lms.djangoapps.certificates.models import ( GeneratedCertificate ) from lms.djangoapps.certificates.permissions import PREVIEW_CERTIFICATES -from lms.djangoapps.certificates.utils import emit_certificate_event, get_certificate_url +from lms.djangoapps.certificates.utils import ( + emit_certificate_event, + get_certificate_url, + get_preferred_certificate_name +) from openedx.core.djangoapps.catalog.api import get_course_run_details from openedx.core.djangoapps.certificates.api import display_date_for_certificate from openedx.core.djangoapps.content.course_overviews.api import get_course_overview_or_none @@ -305,7 +309,8 @@ def _update_context_with_user_info(context, user, user_certificate): """ Updates context dictionary with user related info. """ - user_fullname = user.profile.name + user_fullname = get_preferred_certificate_name(user) + context['username'] = user.username context['course_mode'] = user_certificate.mode context['accomplishment_user_id'] = user.id