diff --git a/common/djangoapps/student/signals/receivers.py b/common/djangoapps/student/signals/receivers.py index 5c4e754f7f..c05813dc2d 100644 --- a/common/djangoapps/student/signals/receivers.py +++ b/common/djangoapps/student/signals/receivers.py @@ -9,7 +9,6 @@ from django.contrib.auth import get_user_model from django.db import IntegrityError from django.db.models.signals import post_save, pre_save from django.dispatch import receiver -from edx_name_affirmation.signals import VERIFIED_NAME_APPROVED from lms.djangoapps.courseware.toggles import courseware_mfe_progress_milestones_are_active from common.djangoapps.student.helpers import EMAIL_EXISTS_MSG_FMT, USERNAME_EXISTS_MSG_FMT, AccountValidationError @@ -21,6 +20,7 @@ from common.djangoapps.student.models import ( is_username_retired ) from common.djangoapps.student.models_api import confirm_name_change +from openedx.features.name_affirmation_api.utils import is_name_affirmation_installed @receiver(pre_save, sender=get_user_model()) @@ -81,7 +81,6 @@ def create_course_enrollment_celebration(sender, instance, created, **kwargs): pass -@receiver(VERIFIED_NAME_APPROVED) def listen_for_verified_name_approved(sender, user_id, profile_name, **kwargs): """ If the user has a pending name change that corresponds to an approved verified name, confirm it. @@ -92,3 +91,9 @@ def listen_for_verified_name_approved(sender, user_id, profile_name, **kwargs): confirm_name_change(user, pending_name_change) except PendingNameChange.DoesNotExist: pass + + +if is_name_affirmation_installed(): + # pylint: disable=import-error + from edx_name_affirmation.signals import VERIFIED_NAME_APPROVED + VERIFIED_NAME_APPROVED.connect(listen_for_verified_name_approved) diff --git a/common/djangoapps/student/tests/test_receivers.py b/common/djangoapps/student/tests/test_receivers.py index 85a45f3cf6..39e654adeb 100644 --- a/common/djangoapps/student/tests/test_receivers.py +++ b/common/djangoapps/student/tests/test_receivers.py @@ -1,6 +1,6 @@ """ Tests for student signal receivers. """ -from edx_name_affirmation.signals import VERIFIED_NAME_APPROVED +from unittest import skipUnless from edx_toggles.toggles.testutils import override_waffle_flag from lms.djangoapps.courseware.toggles import COURSEWARE_MICROFRONTEND_PROGRESS_MILESTONES from common.djangoapps.student.models import ( @@ -13,8 +13,14 @@ from common.djangoapps.student.tests.factories import ( UserFactory, UserProfileFactory ) +from openedx.features.name_affirmation_api.utils import is_name_affirmation_installed from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order +name_affirmation_installed = is_name_affirmation_installed() +if name_affirmation_installed: + # pylint: disable=import-error + from edx_name_affirmation.signals import VERIFIED_NAME_APPROVED + class ReceiversTest(SharedModuleStoreTestCase): """ @@ -47,6 +53,7 @@ class ReceiversTest(SharedModuleStoreTestCase): CourseEnrollmentFactory() assert CourseEnrollmentCelebration.objects.count() == 0 + @skipUnless(name_affirmation_installed, "Requires Name Affirmation") def test_listen_for_verified_name_approved(self): """ Test that profile name is updated when a pending name change is approved diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index ef2deca5e5..3a176e9183 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -18,7 +18,6 @@ from django.db.models import Count from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ -from edx_name_affirmation.api import get_verified_name, should_use_verified_name_for_certs from model_utils import Choices from model_utils.models import TimeStampedModel from opaque_keys.edx.django.models import CourseKeyField @@ -33,6 +32,7 @@ from lms.djangoapps.certificates.data import CertificateStatuses from lms.djangoapps.instructor_task.models import InstructorTask from openedx.core.djangoapps.signals.signals import COURSE_CERT_AWARDED, COURSE_CERT_CHANGED, COURSE_CERT_REVOKED from openedx.core.djangoapps.xmodule_django.models import NoneToEmptyManager +from openedx.features.name_affirmation_api.utils import get_name_affirmation_service from openedx_events.learning.data import CourseData, UserData, UserPersonalData, CertificateData # lint-amnesty, pylint: disable=wrong-import-order from openedx_events.learning.signals import CERTIFICATE_CHANGED, CERTIFICATE_CREATED, CERTIFICATE_REVOKED # lint-amnesty, pylint: disable=wrong-import-order @@ -435,9 +435,10 @@ class GeneratedCertificate(models.Model): a circular dependency. """ name_to_use = student_api.get_name(user.id) + name_affirmation_service = get_name_affirmation_service() - if should_use_verified_name_for_certs(user): - verified_name_obj = get_verified_name(user, is_verified=True) + if name_affirmation_service and name_affirmation_service.should_use_verified_name_for_certs(user): + verified_name_obj = name_affirmation_service.get_verified_name(user, is_verified=True) if verified_name_obj: name_to_use = verified_name_obj.verified_name diff --git a/lms/djangoapps/certificates/tests/test_generation.py b/lms/djangoapps/certificates/tests/test_generation.py index 2246c384cb..6b7f3f1f3f 100644 --- a/lms/djangoapps/certificates/tests/test_generation.py +++ b/lms/djangoapps/certificates/tests/test_generation.py @@ -3,10 +3,7 @@ Tests for certificate generation """ import ddt import logging # lint-amnesty, pylint: disable=wrong-import-order -from unittest import mock # lint-amnesty, pylint: disable=wrong-import-order - -from edx_name_affirmation.api import create_verified_name, create_verified_name_config -from edx_name_affirmation.statuses import VerifiedNameStatus +from unittest import mock, skipUnless # lint-amnesty, pylint: disable=wrong-import-order from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.models import UserProfile @@ -16,12 +13,14 @@ from lms.djangoapps.certificates.data import CertificateStatuses from lms.djangoapps.certificates.generation import generate_course_certificate from lms.djangoapps.certificates.models import GeneratedCertificate from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory +from openedx.features.name_affirmation_api.utils import get_name_affirmation_service 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 log = logging.getLogger(__name__) PROFILE_NAME_METHOD = 'common.djangoapps.student.models_api.get_name' +name_affirmation_service = get_name_affirmation_service() @ddt.ddt @@ -193,9 +192,10 @@ class CertificateTests(EventTestMixin, ModuleStoreTestCase): assert cert.grade == self.grade assert cert.name == '' - @ddt.data((True, VerifiedNameStatus.APPROVED), - (True, VerifiedNameStatus.DENIED), - (False, VerifiedNameStatus.PENDING)) + @skipUnless(name_affirmation_service is not None, 'Requires Name Affirmation') + @ddt.data((True, 'approved'), + (True, 'denied'), + (False, 'pending')) @ddt.unpack def test_generation_verified_name(self, should_use_verified_name_for_certs, status): """ @@ -204,8 +204,11 @@ class CertificateTests(EventTestMixin, ModuleStoreTestCase): their profile name. """ verified_name = 'Jonathan Doe' - create_verified_name(self.u, verified_name, self.name, status=status) - create_verified_name_config(self.u, use_verified_name_for_certs=should_use_verified_name_for_certs) + name_affirmation_service.create_verified_name(self.u, verified_name, self.name, status=status) + name_affirmation_service.create_verified_name_config( + self.u, + use_verified_name_for_certs=should_use_verified_name_for_certs + ) GeneratedCertificateFactory( user=self.u, @@ -220,7 +223,7 @@ class CertificateTests(EventTestMixin, ModuleStoreTestCase): cert = GeneratedCertificate.objects.get(user=self.u, course_id=self.key) - if should_use_verified_name_for_certs and status == VerifiedNameStatus.APPROVED: + if should_use_verified_name_for_certs and status == 'approved': 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 90b3135356..a5623154ea 100644 --- a/lms/djangoapps/certificates/tests/test_models.py +++ b/lms/djangoapps/certificates/tests/test_models.py @@ -3,7 +3,7 @@ import json from unittest.mock import patch -from unittest import mock +from unittest import mock, skipUnless import ddt import pytest @@ -12,8 +12,6 @@ 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.statuses import VerifiedNameStatus from opaque_keys.edx.locator import CourseKey, CourseLocator from openedx_events.tests.utils import OpenEdxEventsTestMixin from path import Path as path @@ -39,6 +37,7 @@ from lms.djangoapps.certificates.tests.factories import ( ) from lms.djangoapps.instructor_task.tests.factories import InstructorTaskFactory from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory +from openedx.features.name_affirmation_api.utils import get_name_affirmation_service from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order @@ -53,6 +52,8 @@ TEST_DATA_DIR = 'common/test/data/' PLATFORM_ROOT = TEST_DIR.parent.parent.parent.parent TEST_DATA_ROOT = PLATFORM_ROOT / TEST_DATA_DIR +name_affirmation_service = get_name_affirmation_service() + class ExampleCertificateTest(TestCase, OpenEdxEventsTestMixin): """Tests for the ExampleCertificate model. """ @@ -624,9 +625,10 @@ class GeneratedCertificateTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin self._assert_event_data(mock_emit_certificate_event, expected_event_data) - @ddt.data((True, VerifiedNameStatus.APPROVED), - (True, VerifiedNameStatus.DENIED), - (False, VerifiedNameStatus.PENDING)) + @skipUnless(name_affirmation_service is not None, 'Requires Name Affirmation') + @ddt.data((True, 'approved'), + (True, 'denied'), + (False, 'pending')) @ddt.unpack def test_invalidate_with_verified_name(self, should_use_verified_name_for_certs, status): """ @@ -634,8 +636,11 @@ class GeneratedCertificateTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin """ verified_name = 'Jonathan Doe' profile = UserProfile.objects.get(user=self.user) - create_verified_name(self.user, verified_name, profile.name, status=status) - create_verified_name_config(self.user, use_verified_name_for_certs=should_use_verified_name_for_certs) + name_affirmation_service.create_verified_name(self.user, verified_name, profile.name, status=status) + name_affirmation_service.create_verified_name_config( + self.user, + use_verified_name_for_certs=should_use_verified_name_for_certs + ) cert = GeneratedCertificateFactory.create( status=CertificateStatuses.downloadable, @@ -649,7 +654,7 @@ class GeneratedCertificateTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin 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 status == VerifiedNameStatus.APPROVED: + if should_use_verified_name_for_certs and status == 'approved': assert cert.name == verified_name else: assert cert.name == profile.name diff --git a/lms/djangoapps/certificates/tests/test_webview_views.py b/lms/djangoapps/certificates/tests/test_webview_views.py index 89d75e866d..fb6625daf0 100644 --- a/lms/djangoapps/certificates/tests/test_webview_views.py +++ b/lms/djangoapps/certificates/tests/test_webview_views.py @@ -2,6 +2,7 @@ import datetime +from unittest import skipUnless from unittest.mock import patch from urllib.parse import urlencode from uuid import uuid4 @@ -11,8 +12,6 @@ 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.statuses import VerifiedNameStatus from edx_toggles.toggles.testutils import override_waffle_switch from organizations import api as organizations_api @@ -51,6 +50,7 @@ from openedx.core.djangoapps.site_configuration.tests.test_util import ( from openedx.core.djangolib.js_utils import js_escaped_string from openedx.core.djangolib.testing.utils import CacheIsolationTestCase 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 @@ -66,6 +66,8 @@ 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): """ @@ -1613,10 +1615,11 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) ) ) + @skipUnless(name_affirmation_service is not None, 'Requires Name Affirmation') @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) - @ddt.data((True, VerifiedNameStatus.APPROVED), - (True, VerifiedNameStatus.DENIED), - (False, VerifiedNameStatus.PENDING)) + @ddt.data((True, 'approved'), + (True, 'denied'), + (False, 'pending')) @ddt.unpack def test_certificate_view_verified_name(self, should_use_verified_name_for_certs, status): """ @@ -1624,8 +1627,16 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) 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, status=status) - create_verified_name_config(self.user, use_verified_name_for_certs=should_use_verified_name_for_certs) + name_affirmation_service.create_verified_name( + self.user, + verified_name, + self.user.profile.name, + status=status + ) + name_affirmation_service.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( @@ -1635,7 +1646,7 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase) ) response = self.client.get(test_url, HTTP_HOST='test.localhost') - if should_use_verified_name_for_certs and status == VerifiedNameStatus.APPROVED: + if should_use_verified_name_for_certs and status == 'approved': self.assertContains(response, verified_name) self.assertNotContains(response, self.user.profile.name) else: diff --git a/lms/djangoapps/certificates/utils.py b/lms/djangoapps/certificates/utils.py index cf557ca3ff..725c54ac09 100644 --- a/lms/djangoapps/certificates/utils.py +++ b/lms/djangoapps/certificates/utils.py @@ -4,8 +4,6 @@ Certificates utilities from datetime import datetime import logging -from edx_name_affirmation.api import get_verified_name, should_use_verified_name_for_certs - from django.conf import settings from django.urls import reverse from eventtracking import tracker @@ -16,6 +14,7 @@ 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 +from openedx.features.name_affirmation_api.utils import get_name_affirmation_service from xmodule.data import CertificatesDisplayBehaviors # lint-amnesty, pylint: disable=wrong-import-order log = logging.getLogger(__name__) @@ -240,9 +239,10 @@ def get_preferred_certificate_name(user): name, or an empty string if it doesn't exist. """ name_to_use = student_api.get_name(user.id) + name_affirmation_service = get_name_affirmation_service() - if should_use_verified_name_for_certs(user): - verified_name_obj = get_verified_name(user, is_verified=True) + if name_affirmation_service and name_affirmation_service.should_use_verified_name_for_certs(user): + verified_name_obj = name_affirmation_service.get_verified_name(user, is_verified=True) if verified_name_obj: name_to_use = verified_name_obj.verified_name diff --git a/openedx/core/djangoapps/user_api/accounts/api.py b/openedx/core/djangoapps/user_api/accounts/api.py index 2412db8bfe..4fa9e76563 100644 --- a/openedx/core/djangoapps/user_api/accounts/api.py +++ b/openedx/core/djangoapps/user_api/accounts/api.py @@ -11,7 +11,6 @@ from django.core.exceptions import ObjectDoesNotExist from django.core.validators import ValidationError, validate_email from django.utils.translation import override as override_language from django.utils.translation import gettext as _ -from edx_name_affirmation.name_change_validator import NameChangeValidator from pytz import UTC from common.djangoapps.student import views as student_views from common.djangoapps.student.models import ( @@ -38,8 +37,14 @@ from openedx.core.djangoapps.user_authn.utils import check_pwned_password from openedx.core.djangoapps.user_authn.views.registration_form import validate_name, validate_username from openedx.core.lib.api.view_utils import add_serializer_errors from openedx.features.enterprise_support.utils import get_enterprise_readonly_account_fields +from openedx.features.name_affirmation_api.utils import is_name_affirmation_installed from .serializers import AccountLegacyProfileSerializer, AccountUserSerializer, UserReadOnlySerializer, _visible_fields +name_affirmation_installed = is_name_affirmation_installed() +if name_affirmation_installed: + # pylint: disable=import-error + from edx_name_affirmation.name_change_validator import NameChangeValidator + # Public access point for this function. visible_fields = _visible_fields @@ -274,6 +279,9 @@ def _does_name_change_require_verification(user_profile, old_name, new_name): """ If name change requires ID verification, do not update it through this API. """ + if not name_affirmation_installed: + return False + profile_meta = user_profile.get_meta() old_names_list = profile_meta['old_names'] if 'old_names' in profile_meta else [] diff --git a/openedx/core/djangoapps/user_api/accounts/serializers.py b/openedx/core/djangoapps/user_api/accounts/serializers.py index 2e0d78c182..c416fdd5f7 100644 --- a/openedx/core/djangoapps/user_api/accounts/serializers.py +++ b/openedx/core/djangoapps/user_api/accounts/serializers.py @@ -11,7 +11,6 @@ from django.conf import settings from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.core.exceptions import ObjectDoesNotExist from django.urls import reverse -from edx_name_affirmation.api import get_verified_name from rest_framework import serializers @@ -29,6 +28,7 @@ from openedx.core.djangoapps.user_api.accounts.utils import is_secondary_email_f from openedx.core.djangoapps.user_api.models import RetirementState, UserPreference, UserRetirementStatus from openedx.core.djangoapps.user_api.serializers import ReadOnlyFieldsSerializerMixin from openedx.core.djangoapps.user_authn.views.registration_form import contains_html, contains_url +from openedx.features.name_affirmation_api.utils import get_name_affirmation_service from . import ( ACCOUNT_VISIBILITY_PREF_KEY, @@ -170,11 +170,10 @@ class UserReadOnlySerializer(serializers.Serializer): # lint-amnesty, pylint: d "extended_profile_fields": None, "phone_number": None, "pending_name_change": None, + "verified_name": None, } if user_profile: - verified_name_obj = get_verified_name(user, is_verified=True) - verified_name = verified_name_obj.verified_name if verified_name_obj else None data.update( { "bio": AccountLegacyProfileSerializer.convert_empty_to_None(user_profile.bio), @@ -187,7 +186,6 @@ class UserReadOnlySerializer(serializers.Serializer): # lint-amnesty, pylint: d user_profile.language_proficiencies.all().order_by('code'), many=True ).data, "name": user_profile.name, - "verified_name": verified_name, "gender": AccountLegacyProfileSerializer.convert_empty_to_None(user_profile.gender), "goals": user_profile.goals, "year_of_birth": user_profile.year_of_birth, @@ -211,6 +209,12 @@ class UserReadOnlySerializer(serializers.Serializer): # lint-amnesty, pylint: d except PendingNameChange.DoesNotExist: pass + name_affirmation_service = get_name_affirmation_service() + if name_affirmation_service: + verified_name_obj = name_affirmation_service.get_verified_name(user, is_verified=True) + if verified_name_obj: + data.update({"verified_name": verified_name_obj.verified_name}) + if is_secondary_email_feature_enabled(): data.update( { diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py index 87cd3ae647..65839d8375 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py @@ -14,8 +14,6 @@ from django.conf import settings from django.test.testcases import TransactionTestCase from django.test.utils import override_settings from django.urls import reverse -from edx_name_affirmation.api import create_verified_name -from edx_name_affirmation.statuses import VerifiedNameStatus from rest_framework import status from rest_framework.test import APIClient, APITestCase @@ -26,6 +24,7 @@ from openedx.core.djangoapps.user_api.accounts import ACCOUNT_VISIBILITY_PREF_KE from openedx.core.djangoapps.user_api.models import UserPreference from openedx.core.djangoapps.user_api.preferences.api import set_user_preference from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms +from openedx.features.name_affirmation_api.utils import get_name_affirmation_service from .. import ALL_USERS_VISIBILITY, CUSTOM_VISIBILITY, PRIVATE_VISIBILITY @@ -55,6 +54,7 @@ class UserAPITestCase(APITestCase): self.staff_user = UserFactory(is_staff=True, password=TEST_PASSWORD) self.staff_client = APIClient() self.user = UserFactory.create(password=TEST_PASSWORD) # will be assigned to self.client by default + self.name_affirmation_service = get_name_affirmation_service() def login_client(self, api_client, user): """Helper method for getting the client and user and logging in. Returns client. """ @@ -142,9 +142,16 @@ class UserAPITestCase(APITestCase): def create_mock_verified_name(self, user): """ Helper method to create an approved VerifiedName entry in name affirmation. + Will not do anything if Name Affirmation is not installed. """ - legacy_profile = UserProfile.objects.get(id=user.id) - create_verified_name(user, self.VERIFIED_NAME, legacy_profile.name, status=VerifiedNameStatus.APPROVED) + if self.name_affirmation_service: + legacy_profile = UserProfile.objects.get(id=user.id) + self.name_affirmation_service.create_verified_name( + user, + self.VERIFIED_NAME, + legacy_profile.name, + status='approved' + ) def create_user_registration(self, user): """ @@ -152,6 +159,14 @@ class UserAPITestCase(APITestCase): """ RegistrationFactory(user=user) + def _get_num_queries(self, num_queries): + """ + If Name Affirmation is installed, it will add an extra query + """ + if self.name_affirmation_service: + return num_queries + 1 + return num_queries + def _verify_profile_image_data(self, data, has_profile_image): """ Verify the profile image data in a GET response for self.user @@ -240,7 +255,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): """ ENABLED_CACHES = ['default'] - TOTAL_QUERY_COUNT = 27 + TOTAL_QUERY_COUNT = 26 FULL_RESPONSE_FIELD_COUNT = 30 def setUp(self): @@ -324,7 +339,6 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): # additional admin fields (13) assert self.user.email == data['email'] assert self.user.id == data['id'] - assert self.VERIFIED_NAME == data['verified_name'] assert data['extended_profile'] is not None assert 'MA' == data['state'] assert 'f' == data['gender'] @@ -335,6 +349,10 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): assert data['secondary_email'] is None assert data['secondary_email_enabled'] is None assert year_of_birth == data['year_of_birth'] + if self.name_affirmation_service: + assert self.VERIFIED_NAME == data['verified_name'] + else: + assert data['verified_name'] is None def test_anonymous_access(self): """ @@ -501,7 +519,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): """ self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD) self.create_mock_profile(self.user) - with self.assertNumQueries(self.TOTAL_QUERY_COUNT): + with self.assertNumQueries(self._get_num_queries(self.TOTAL_QUERY_COUNT)): response = self.send_get(self.different_client) self._verify_full_shareable_account_response(response, account_privacy=ALL_USERS_VISIBILITY) @@ -516,7 +534,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): """ self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD) self.create_mock_profile(self.user) - with self.assertNumQueries(self.TOTAL_QUERY_COUNT): + with self.assertNumQueries(self._get_num_queries(self.TOTAL_QUERY_COUNT)): response = self.send_get(self.different_client) self._verify_private_account_response(response) @@ -667,12 +685,12 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): assert data['accomplishments_shared'] is False self.client.login(username=self.user.username, password=TEST_PASSWORD) - verify_get_own_information(25) + verify_get_own_information(self._get_num_queries(24)) # Now make sure that the user can get the same information, even if not active self.user.is_active = False self.user.save() - verify_get_own_information(17) + verify_get_own_information(self._get_num_queries(16)) def test_get_account_empty_string(self): """ @@ -687,7 +705,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): legacy_profile.save() self.client.login(username=self.user.username, password=TEST_PASSWORD) - with self.assertNumQueries(25): + with self.assertNumQueries(self._get_num_queries(24)): response = self.send_get(self.client) for empty_field in ("level_of_education", "gender", "country", "state", "bio",): assert response.data[empty_field] is None diff --git a/openedx/features/name_affirmation_api/apps.py b/openedx/features/name_affirmation_api/apps.py index 35cdb150a7..0d7d092997 100644 --- a/openedx/features/name_affirmation_api/apps.py +++ b/openedx/features/name_affirmation_api/apps.py @@ -7,10 +7,12 @@ from django.apps import AppConfig from django.conf import settings from edx_proctoring.runtime import set_runtime_service +from openedx.features.name_affirmation_api.utils import get_name_affirmation_service + class NameAffirmationApiConfig(AppConfig): """ - Application Configuration for Misc Services. + Application Configuration for Name Affirmation API. """ name = 'openedx.features.name_affirmation_api' @@ -19,5 +21,6 @@ class NameAffirmationApiConfig(AppConfig): Connect services. """ if settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'): - from edx_name_affirmation.services import NameAffirmationService - set_runtime_service('name_affirmation', NameAffirmationService()) + name_affirmation_service = get_name_affirmation_service() + if name_affirmation_service: + set_runtime_service('name_affirmation', name_affirmation_service) diff --git a/openedx/features/name_affirmation_api/tests/__init__.py b/openedx/features/name_affirmation_api/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/features/name_affirmation_api/tests/test_utils.py b/openedx/features/name_affirmation_api/tests/test_utils.py new file mode 100644 index 0000000000..f157b95d9d --- /dev/null +++ b/openedx/features/name_affirmation_api/tests/test_utils.py @@ -0,0 +1,36 @@ +""" Tests for Name Affirmation API utils """ + +from unittest import TestCase +from unittest.mock import patch + +import ddt +from edx_django_utils.plugins import PluginError + +from openedx.features.name_affirmation_api.utils import is_name_affirmation_installed, get_name_affirmation_service + + +@ddt.ddt +class TestNameAffirmationAPIUtils(TestCase): + """ Tests for Name Affirmation API utils """ + + @patch('openedx.features.name_affirmation_api.utils.PluginManager') + def test_name_affirmation_installed(self, mock_manager): + mock_manager.get_plugin.return_value = 'mock plugin' + self.assertTrue(is_name_affirmation_installed()) + + @patch('openedx.features.name_affirmation_api.utils.PluginManager') + def test_name_affirmation_not_installed(self, mock_manager): + mock_manager.side_effect = PluginError('No such plugin') + with self.assertRaises(PluginError): + self.assertFalse(is_name_affirmation_installed()) + + @patch('edx_name_affirmation.services.NameAffirmationService') + @ddt.data(True, False) + def test_get_name_affirmation_service(self, name_affirmation_installed, mock_service): + with patch('openedx.features.name_affirmation_api.utils.is_name_affirmation_installed', + return_value=name_affirmation_installed): + name_affirmation_service = get_name_affirmation_service() + if name_affirmation_installed: + self.assertEqual(name_affirmation_service, mock_service()) + else: + self.assertIsNone(name_affirmation_service) diff --git a/openedx/features/name_affirmation_api/utils.py b/openedx/features/name_affirmation_api/utils.py new file mode 100644 index 0000000000..b9f4ee8d9c --- /dev/null +++ b/openedx/features/name_affirmation_api/utils.py @@ -0,0 +1,31 @@ +""" +Utility functions for integration with Name Affirmation plugin +(https://github.com/edx/edx-name-affirmation) +""" + +from edx_django_utils.plugins import PluginError, PluginManager + + +def is_name_affirmation_installed(): + """ + Returns boolean describing whether Name Affirmation plugin is installed. + """ + manager = PluginManager() + try: + plugin = manager.get_plugin('edx_name_affirmation', 'lms.djangoapp') + return bool(plugin) + except PluginError: + return False + + +def get_name_affirmation_service(): + """ + Returns Name Affirmation service which exposes API . + If Name Affirmation is not installed, return None. + """ + if is_name_affirmation_installed(): + # pylint: disable=import-error + from edx_name_affirmation.services import NameAffirmationService + return NameAffirmationService() + + return None