From 9e83bf8f94e40773b93484771ab2834dae65a5fb Mon Sep 17 00:00:00 2001 From: Justin Hynes Date: Wed, 7 Sep 2022 13:08:12 -0400 Subject: [PATCH] feat: Add configuration option to route `View Records` button to the Learner Record MFE [APER-1922] We are converting the legacy UI of the `records` app in the Credentials IDA (credentials.edx.org/records/, credentials.edx.org/records/programs/{uuid}, etc.) to a new MFE. Today, the Program Dashboard and the legacy (non-MFE) profile page have buttons that route learners to the Credentials IDA pages. We need to (optionally) introduce a way to route learner's to the new MFE instead. - Introduces a new configuration setting called `LEARNER_RECORD_MICROFRONTEND_URL` (defaulting to None). This will be used by the LMS to store the base URL of the new MFE (e.g. records.stage.edx.org). - Introduces a new waffle switch named `USE_LEARNER_RECORD_MFE`. This will be used to control whether routing learner's to the new MFE is enabled from the LMS's side. - Updates the existing `get_credentials_records_url` function to add additional logic that will determine if we need to build a link to the legacy FE or the new MFE - Adds tests for new and existing behavior. There were no existing unit tests for the utility function that I updated. --- lms/envs/common.py | 11 ++- lms/envs/devstack.py | 1 + openedx/core/djangoapps/credentials/config.py | 17 ++++ .../credentials/tests/test_utils.py | 80 +++++++++++++++++-- openedx/core/djangoapps/credentials/utils.py | 27 +++++-- 5 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 openedx/core/djangoapps/credentials/config.py diff --git a/lms/envs/common.py b/lms/envs/common.py index f8347a0403..830e5153a3 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -4887,14 +4887,21 @@ LEARNING_MICROFRONTEND_URL = None # waffle flag. ORA_GRADING_MICROFRONTEND_URL = None # .. setting_name: DISCUSSIONS_MICROFRONTEND_URL -# .. setting_description: Base URL of the micro-frontend-based discussions page. # .. setting_default: None +# .. setting_description: Base URL of the micro-frontend-based discussions page. # .. setting_warning: Also set site's courseware.discussions_mfe waffle flag. DISCUSSIONS_MICROFRONTEND_URL = None -# .. setting_name: DISCUSSIONS_MFE_FEEDBACK_URL = None +# .. setting_name: DISCUSSIONS_MFE_FEEDBACK_URL # .. setting_default: None # .. setting_description: Base URL of the discussions micro-frontend google form based feedback. DISCUSSIONS_MFE_FEEDBACK_URL = None +# .. setting_name: LEARNER_RECORD_MFE_URL +# .. setting_default: None +# .. setting_description: Base URL of the micro-frontend responsible for displaying Learner Record and Program record +# pages. This MFE replaces the legacy frontend originally offered in the Credentials IDA. +# .. setting_warning: In order to route requests to the MFE correctly you must also create and enable the credentials +# app's `USE_LEARNER_RECORD_MFE` waffle flag. See openedx/core/djangoapps/credentials/config.py. +LEARNER_RECORD_MICROFRONTEND_URL = None # .. toggle_name: ENABLE_AUTHN_RESET_PASSWORD_HIBP_POLICY # .. toggle_implementation: DjangoSetting # .. toggle_default: False diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index 54e7bb5c41..add2ed0538 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -362,6 +362,7 @@ ACCOUNT_MICROFRONTEND_URL = 'http://localhost:1997' COMMUNICATIONS_MICROFRONTEND_URL = 'http://localhost:1984' AUTHN_MICROFRONTEND_URL = 'http://localhost:1999' AUTHN_MICROFRONTEND_DOMAIN = 'localhost:1999' +LEARNER_RECORD_MICROFRONTEND_URL = 'http://localhost:1990' ################### FRONTEND APPLICATION DISCUSSIONS ################### DISCUSSIONS_MICROFRONTEND_URL = 'http://localhost:2002' diff --git a/openedx/core/djangoapps/credentials/config.py b/openedx/core/djangoapps/credentials/config.py new file mode 100644 index 0000000000..159654be9f --- /dev/null +++ b/openedx/core/djangoapps/credentials/config.py @@ -0,0 +1,17 @@ +""" +This module contains various configuration settings via waffle switches for the Credentials app. +""" + +from edx_toggles.toggles import WaffleSwitch + +# Namespace +WAFFLE_NAMESPACE = 'credentials' + +# .. toggle_name: credentials.use_learner_record_mfe +# .. toggle_implementation: WaffleSwitch +# .. toggle_default: False +# .. toggle_description: This toggle will inform the Program Dashboard to route to the Learner Record MFE over the +# legacy frontend of the Credentials IDA. +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2022-09-07 +USE_LEARNER_RECORD_MFE = WaffleSwitch(f"{WAFFLE_NAMESPACE}.use_learner_record_mfe", __name__) diff --git a/openedx/core/djangoapps/credentials/tests/test_utils.py b/openedx/core/djangoapps/credentials/tests/test_utils.py index ed8879386e..49a26e2ac8 100644 --- a/openedx/core/djangoapps/credentials/tests/test_utils.py +++ b/openedx/core/djangoapps/credentials/tests/test_utils.py @@ -1,14 +1,15 @@ """Tests covering Credentials utilities.""" - - import uuid - from unittest import mock +from django.test import override_settings +from edx_toggles.toggles.testutils import override_waffle_switch + +from openedx.core.djangoapps.credentials.config import USE_LEARNER_RECORD_MFE from openedx.core.djangoapps.credentials.models import CredentialsApiConfig from openedx.core.djangoapps.credentials.tests import factories from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin -from openedx.core.djangoapps.credentials.utils import get_credentials +from openedx.core.djangoapps.credentials.utils import get_credentials, get_credentials_records_url from openedx.core.djangoapps.oauth_dispatch.tests.factories import ApplicationFactory from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms from common.djangoapps.student.tests.factories import UserFactory @@ -17,7 +18,6 @@ UTILS_MODULE = 'openedx.core.djangoapps.credentials.utils' @skip_unless_lms -@mock.patch(UTILS_MODULE + '.get_api_data') class TestGetCredentials(CredentialsApiConfigMixin, CacheIsolationTestCase): """ Tests for credentials utility functions. """ @@ -31,6 +31,7 @@ class TestGetCredentials(CredentialsApiConfigMixin, CacheIsolationTestCase): self.credentials_config = self.create_credentials_config(cache_ttl=1) self.user = UserFactory() + @mock.patch(UTILS_MODULE + '.get_api_data') def test_get_many(self, mock_get_edx_api_data): expected = factories.UserCredential.create_batch(3) mock_get_edx_api_data.return_value = expected @@ -52,6 +53,7 @@ class TestGetCredentials(CredentialsApiConfigMixin, CacheIsolationTestCase): assert actual == expected + @mock.patch(UTILS_MODULE + '.get_api_data') def test_get_one(self, mock_get_edx_api_data): expected = factories.UserCredential() mock_get_edx_api_data.return_value = expected @@ -75,6 +77,7 @@ class TestGetCredentials(CredentialsApiConfigMixin, CacheIsolationTestCase): assert actual == expected + @mock.patch(UTILS_MODULE + '.get_api_data') def test_type_filter(self, mock_get_edx_api_data): get_credentials(self.user, credential_type='program') @@ -89,3 +92,70 @@ class TestGetCredentials(CredentialsApiConfigMixin, CacheIsolationTestCase): 'type': 'program', } assert kwargs['querystring'] == querystring + + @override_settings(LEARNER_RECORD_MICROFRONTEND_URL=None) + @override_settings(CREDENTIALS_PUBLIC_SERVICE_URL="http://foo") + def test_get_credentials_records_url(self): + """ + A test that verifies the functionality of the `get_credentials_records_url`.  + """ + result = get_credentials_records_url() + assert result == "http://foo/records/" + + result = get_credentials_records_url("abcdefgh-ijkl-mnop-qrst-uvwxyz123456") + assert result == "http://foo/records/programs/abcdefghijklmnopqrstuvwxyz123456/" + + @override_settings(LEARNER_RECORD_MICROFRONTEND_URL="http://blah") + @override_settings(CREDENTIALS_PUBLIC_SERVICE_URL="http://foo") + @override_waffle_switch(USE_LEARNER_RECORD_MFE, False) + def test_get_credentials_records_mfe_url_waffle_disabled(self): + """ + A test that verifies the results of the `get_credentials_records_url` function when the + LEARNER_RECORD_MICROFRONTEND_URL setting exists but the USE_LEARNER_RECORD_MFE waffle flag is disabled. + """ + result = get_credentials_records_url() + assert result == "http://foo/records/" + + result = get_credentials_records_url("abcdefgh-ijkl-mnop-qrst-uvwxyz123456") + assert result == "http://foo/records/programs/abcdefghijklmnopqrstuvwxyz123456/" + + @override_settings(LEARNER_RECORD_MICROFRONTEND_URL="http://blah") + @override_settings(CREDENTIALS_PUBLIC_SERVICE_URL="http://foo") + @override_waffle_switch(USE_LEARNER_RECORD_MFE, True) + def test_get_credentials_records_mfe_url_waffle_enabled(self): + """ + A test that verifies the results of the `get_credentials_records_url` function when the + LEARNER_RECORD_MICROFRONTEND_URL setting exists but the USE_LEARNER_RECORD_MFE waffle flag is enabled. + """ + result = get_credentials_records_url() + assert result == "http://blah/" + + result = get_credentials_records_url("abcdefgh-ijkl-mnop-qrst-uvwxyz123456") + assert result == "http://blah/abcdefghijklmnopqrstuvwxyz123456/" + + @override_settings(CREDENTIALS_PUBLIC_SERVICE_URL=None) + @override_settings(LEARNER_RECORD_MICROFRONTEND_URL=None) + def test_get_credentials_records_url_expect_none(self): + """ + A test that verifieis the results of the `get_credentials_records_url` function when the system is configured + to use neither the Credentials IDA or the Learner Record MFE. + """ + result = get_credentials_records_url() + assert result is None + + result = get_credentials_records_url("abcdefgh-ijkl-mnop-qrst-uvwxyz123456") + assert result is None + + @override_settings(LEARNER_RECORD_MICROFRONTEND_URL="http://blah") + @override_settings(CREDENTIALS_PUBLIC_SERVICE_URL=None) + @override_waffle_switch(USE_LEARNER_RECORD_MFE, True) + def test_get_credentials_records_url_only_mfe_configured(self): + """ + A test that verifieis the results of the `get_credentials_records_url` function when the system is configured + to use only the Learner Record MFE. + """ + result = get_credentials_records_url() + assert result == "http://blah/" + + result = get_credentials_records_url("abcdefgh-ijkl-mnop-qrst-uvwxyz123456") + assert result == "http://blah/abcdefghijklmnopqrstuvwxyz123456/" diff --git a/openedx/core/djangoapps/credentials/utils.py b/openedx/core/djangoapps/credentials/utils.py index 2b52b1f5e3..752d41bc4b 100644 --- a/openedx/core/djangoapps/credentials/utils.py +++ b/openedx/core/djangoapps/credentials/utils.py @@ -2,6 +2,9 @@ import requests from edx_rest_api_client.auth import SuppliedJwtAuth +from django.conf import settings + +from openedx.core.djangoapps.credentials.config import USE_LEARNER_RECORD_MFE from openedx.core.djangoapps.credentials.models import CredentialsApiConfig from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user from openedx.core.lib.edx_api_utils import get_api_data @@ -15,13 +18,27 @@ def get_credentials_records_url(program_uuid=None): Arguments: program_uuid (str): Optional program uuid to link for a program records URL """ - base_url = CredentialsApiConfig.current().public_records_url - if base_url is None: + base_url = settings.CREDENTIALS_PUBLIC_SERVICE_URL + learner_record_mfe_base_url = settings.LEARNER_RECORD_MICROFRONTEND_URL + use_learner_record_mfe = USE_LEARNER_RECORD_MFE.is_enabled() and learner_record_mfe_base_url + + if not base_url and not use_learner_record_mfe: return None + + # If we have a program uuid we build a link to the appropriate Program Record page in Credentials (or the Learner + # Record MFE) if program_uuid: - # Credentials expects the uuid without dashes so we are converting here - return base_url + 'programs/{}/'.format(program_uuid.replace('-', '')) - return base_url + # Credentials expects the UUID without dashes so we strip them here + stripped_program_uuid = program_uuid.replace('-', '') + if use_learner_record_mfe: + return f"{learner_record_mfe_base_url}/{stripped_program_uuid}/" + return f"{base_url}/records/programs/{stripped_program_uuid}/" + else: + # Otherwise, build a link to the appropriate Learner Record index page + if use_learner_record_mfe: + return f"{learner_record_mfe_base_url}/" + else: + return f"{base_url}/records/" def get_credentials_api_client(user):