From 5ab865ec0eeeaf812ea5c2ebaecccae7add27729 Mon Sep 17 00:00:00 2001 From: Farhaan Bukhsh Date: Wed, 22 Mar 2023 16:09:24 +0530 Subject: [PATCH 1/5] fix: Update the edx-ora2 requirements Signed-off-by: Farhaan Bukhsh --- requirements/edx/base.txt | 4 ++-- requirements/edx/development.txt | 4 ++-- requirements/edx/testing.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 370e495574e3..5388a5ccef22 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -740,8 +740,8 @@ openedx-events==0.8.1 # via -r requirements/edx/base.in openedx-filters==0.8.0 # via -r requirements/edx/base.in -# TODO: Backport https://github.com/openedx/edx-ora2/pull/1869 to Olive if it uses a lower version than 4.5.0. -ora2 @ git+https://github.com/open-craft/edx-ora2@agrendalath/bb-6151-nutmeg_backport +# TODO: edx-ora2 doesn't support signature and region https://discuss.openedx.org/t/ora2-s3-backend-configuration/9624 +ora2 @ git+https://github.com/open-craft/edx-ora2@farhaan/bb-7224-fix-region-signature-based-url # via -r requirements/edx/base.in packaging==21.3 # via diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 27c90db72ba7..6883ba9ed776 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -982,8 +982,8 @@ openedx-events==0.8.1 # via -r requirements/edx/testing.txt openedx-filters==0.8.0 # via -r requirements/edx/testing.txt -# TODO: Backport https://github.com/openedx/edx-ora2/pull/1869 to Olive if it uses a lower version than 4.5.0. -ora2 @ git+https://github.com/open-craft/edx-ora2@agrendalath/bb-6151-nutmeg_backport +# TODO: edx-ora2 doesn't support signature and region https://discuss.openedx.org/t/ora2-s3-backend-configuration/9624 +ora2 @ git+https://github.com/open-craft/edx-ora2@farhaan/bb-7224-fix-region-signature-based-url # via -r requirements/edx/testing.txt packaging==21.3 # via diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 3cd11d29b2c3..2d55a9d89a49 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -927,8 +927,8 @@ openedx-events==0.8.1 # via -r requirements/edx/base.txt openedx-filters==0.8.0 # via -r requirements/edx/base.txt -# TODO: Backport https://github.com/openedx/edx-ora2/pull/1869 to Olive if it uses a lower version than 4.5.0. -ora2 @ git+https://github.com/open-craft/edx-ora2@agrendalath/bb-6151-nutmeg_backport +# TODO: edx-ora2 doesn't support signature and region https://discuss.openedx.org/t/ora2-s3-backend-configuration/9624 +ora2 @ git+https://github.com/open-craft/edx-ora2@farhaan/bb-7224-fix-region-signature-based-url # via -r requirements/edx/base.txt packaging==21.3 # via From 93c04a780c446e361ed40c801d683dbd4c7011b6 Mon Sep 17 00:00:00 2001 From: Felipe Montoya Date: Fri, 8 Jul 2022 18:04:26 +0300 Subject: [PATCH 2/5] Merge pull request #30473 from eduNEXT/mfmz/mfe-config-api feat: add mfe config api (cherry picked from commit 0bb4577795e8cebf040fbd495450ff710587dded) --- .github/workflows/pylint-checks.yml | 2 +- .github/workflows/unit-test-shards.json | 1 + lms/djangoapps/mfe_config_api/__init__.py | 0 .../docs/decisions/0001-mfe-config-api.rst | 72 ++++++++++ .../mfe_config_api/tests/__init__.py | 0 .../mfe_config_api/tests/test_views.py | 135 ++++++++++++++++++ lms/djangoapps/mfe_config_api/urls.py | 10 ++ lms/djangoapps/mfe_config_api/views.py | 52 +++++++ lms/envs/common.py | 39 +++++ lms/envs/test.py | 13 ++ lms/urls.py | 5 + 11 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 lms/djangoapps/mfe_config_api/__init__.py create mode 100644 lms/djangoapps/mfe_config_api/docs/decisions/0001-mfe-config-api.rst create mode 100644 lms/djangoapps/mfe_config_api/tests/__init__.py create mode 100644 lms/djangoapps/mfe_config_api/tests/test_views.py create mode 100644 lms/djangoapps/mfe_config_api/urls.py create mode 100644 lms/djangoapps/mfe_config_api/views.py diff --git a/.github/workflows/pylint-checks.yml b/.github/workflows/pylint-checks.yml index 344119d3122a..2c3f182da9d4 100644 --- a/.github/workflows/pylint-checks.yml +++ b/.github/workflows/pylint-checks.yml @@ -17,7 +17,7 @@ jobs: - module-name: lms-1 path: "lms/djangoapps/badges/ lms/djangoapps/branding/ lms/djangoapps/bulk_email/ lms/djangoapps/bulk_enroll/ lms/djangoapps/bulk_user_retirement/ lms/djangoapps/ccx/ lms/djangoapps/certificates/ lms/djangoapps/commerce/ lms/djangoapps/course_api/ lms/djangoapps/course_blocks/ lms/djangoapps/course_home_api/ lms/djangoapps/course_wiki/ lms/djangoapps/coursewarehistoryextended/ lms/djangoapps/debug/ lms/djangoapps/courseware/ lms/djangoapps/course_goals/ lms/djangoapps/rss_proxy/ lms/djangoapps/save_for_later/" - module-name: lms-2 - path: "lms/djangoapps/gating/ lms/djangoapps/grades/ lms/djangoapps/instructor/ lms/djangoapps/instructor_analytics/ lms/djangoapps/discussion/ lms/djangoapps/edxnotes/ lms/djangoapps/email_marketing/ lms/djangoapps/experiments/ lms/djangoapps/instructor_task/ lms/djangoapps/learner_dashboard/ lms/djangoapps/lms_initialization/ lms/djangoapps/lms_xblock/ lms/djangoapps/lti_provider/ lms/djangoapps/mailing/ lms/djangoapps/mobile_api/ lms/djangoapps/monitoring/ lms/djangoapps/ora_staff_grader/ lms/djangoapps/program_enrollments/ lms/djangoapps/rss_proxy lms/djangoapps/static_template_view/ lms/djangoapps/staticbook/ lms/djangoapps/support/ lms/djangoapps/survey/ lms/djangoapps/teams/ lms/djangoapps/tests/ lms/djangoapps/user_tours/ lms/djangoapps/verify_student/ lms/envs/ lms/lib/ lms/tests.py" + path: "lms/djangoapps/gating/ lms/djangoapps/grades/ lms/djangoapps/instructor/ lms/djangoapps/instructor_analytics/ lms/djangoapps/discussion/ lms/djangoapps/edxnotes/ lms/djangoapps/email_marketing/ lms/djangoapps/experiments/ lms/djangoapps/instructor_task/ lms/djangoapps/learner_dashboard/ lms/djangoapps/lms_initialization/ lms/djangoapps/lms_xblock/ lms/djangoapps/lti_provider/ lms/djangoapps/mailing/ lms/djangoapps/mobile_api/ lms/djangoapps/monitoring/ lms/djangoapps/ora_staff_grader/ lms/djangoapps/program_enrollments/ lms/djangoapps/rss_proxy lms/djangoapps/static_template_view/ lms/djangoapps/staticbook/ lms/djangoapps/support/ lms/djangoapps/survey/ lms/djangoapps/teams/ lms/djangoapps/tests/ lms/djangoapps/user_tours/ lms/djangoapps/verify_student/ lms/djangoapps/mfe_config_api/ lms/envs/ lms/lib/ lms/tests.py" - module-name: openedx-1 path: "openedx/core/types/ openedx/core/djangoapps/ace_common/ openedx/core/djangoapps/agreements/ openedx/core/djangoapps/api_admin/ openedx/core/djangoapps/auth_exchange/ openedx/core/djangoapps/bookmarks/ openedx/core/djangoapps/cache_toolbox/ openedx/core/djangoapps/catalog/ openedx/core/djangoapps/ccxcon/ openedx/core/djangoapps/commerce/ openedx/core/djangoapps/common_initialization/ openedx/core/djangoapps/common_views/ openedx/core/djangoapps/config_model_utils/ openedx/core/djangoapps/content/ openedx/core/djangoapps/content_libraries/ openedx/core/djangoapps/contentserver/ openedx/core/djangoapps/cookie_metadata/ openedx/core/djangoapps/cors_csrf/ openedx/core/djangoapps/course_apps/ openedx/core/djangoapps/course_date_signals/ openedx/core/djangoapps/course_groups/ openedx/core/djangoapps/courseware_api/ openedx/core/djangoapps/crawlers/ openedx/core/djangoapps/credentials/ openedx/core/djangoapps/credit/ openedx/core/djangoapps/dark_lang/ openedx/core/djangoapps/debug/ openedx/core/djangoapps/demographics/ openedx/core/djangoapps/discussions/ openedx/core/djangoapps/django_comment_common/ openedx/core/djangoapps/embargo/ openedx/core/djangoapps/enrollments/ openedx/core/djangoapps/external_user_ids/ openedx/core/djangoapps/zendesk_proxy/ openedx/core/djangolib/ openedx/core/lib/ openedx/core/tests/ openedx/core/djangoapps/course_live/" - module-name: openedx-2 diff --git a/.github/workflows/unit-test-shards.json b/.github/workflows/unit-test-shards.json index 838d0fe261f2..ae0950e935a3 100644 --- a/.github/workflows/unit-test-shards.json +++ b/.github/workflows/unit-test-shards.json @@ -71,6 +71,7 @@ "lms/djangoapps/tests/", "lms/djangoapps/user_tours/", "lms/djangoapps/verify_student/", + "lms/djangoapps/mfe_config_api/", "lms/envs/", "lms/lib/", "lms/tests.py" diff --git a/lms/djangoapps/mfe_config_api/__init__.py b/lms/djangoapps/mfe_config_api/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/lms/djangoapps/mfe_config_api/docs/decisions/0001-mfe-config-api.rst b/lms/djangoapps/mfe_config_api/docs/decisions/0001-mfe-config-api.rst new file mode 100644 index 000000000000..d26a40dc9ae6 --- /dev/null +++ b/lms/djangoapps/mfe_config_api/docs/decisions/0001-mfe-config-api.rst @@ -0,0 +1,72 @@ +0001 MFE CONFIG API +#################### + +Status +****** + +Accepted + +Context +******* + +Currently, MFE settings are set via command line environment variables or an .env file that is read during the build process, causing the operators to rebuild mfes each time when any variables are changed. The creation of the ``mfe_config_api`` allows configuration at runtime and avoids rebuilds. +`MFE Configuration during Runtime`_. + +Decision +******** + +- A lightweight API will be created that returns the mfe configuration variables from the site configuration or django settings. `PR Discussion about django settings`_ +- The API will be enabled or disabled using the setting ``ENABLE_MFE_CONFIG_API``. +- The API will take the mfe configuration in the ``MFE_CONFIG`` keyset in the site configuration (admin > site configuration > your domain) or in django settings. +- This API allows to consult the configurations by specific MFE. Making a request like ``api/v1/mfe_config?mfe=mymfe`` will return the configuration defined in ``MFE_CONFIG_MYMFE`` merged with the ``MFE_CONFIG`` configuration. +- The API will have a mechanism to cache the response with ``MFE_CONFIG_API_CACHE_TIMEOUT`` variable. +- The API will live in lms/djangoapps because this is not something Studio needs to serve and it is a lightweight API. `PR Discussion`_ +- The API will not require authentication or authorization. +- The API request and response will be like: + +Request:: + + GET http://lms.base.com/api/v1/mfe_config?mfe=learning + +Response:: + + { + "BASE_URL": "https://name_of_mfe.example.com", + "LANGUAGE_PREFERENCE_COOKIE_NAME": "example-language-preference", + "CREDENTIALS_BASE_URL": "https://credentials.example.com", + "DISCOVERY_API_BASE_URL": "https://discovery.example.com", + "LMS_BASE_URL": "https://courses.example.com", + "LOGIN_URL": "https://courses.example.com/login", + "LOGOUT_URL": "https://courses.example.com/logout", + "STUDIO_BASE_URL": "https://studio.example.com", + "LOGO_URL": "https://courses.example.com/logo.png" + + } + +Consequences +************ + +- We have to change all the mfes so that they take the information from the API. `Issue MFE runtime configuration in frontend-wg`_ +- Initialize the MFE could have a delay due to the HTTP method. +- `Site configuration is going to be deprecated`_ so later we have to clean the code that uses site configuration. +- The operator is responsible for configuring the settings in site configuration or django settings. +- We can have duplicate keys in site configuration (example: we can have a logo definition for each mfe). +- If the request is made from a domain that does not have a site configuration, it returns django settings. + +Rejected Alternatives +********************** + +- It was not made as a plugin or IDA because it is a lightweight implementation `PR Discussion`_ + +References +********** + +.. _MFE Configuration during Runtime: https://docs.google.com/document/d/1-FHIQmyeQZu3311x8eYUNMru4JX7Yb3UlqjmJxvM8do/edit?usp=sharing + +.. _PR Discussion: https://github.com/openedx/edx-platform/pull/30473#issuecomment-1146176151 + +.. _Site configuration is going to be deprecated: https://github.com/openedx/platform-roadmap/issues/21 + +.. _Issue MFE runtime configuration in frontend-wg: https://github.com/openedx/frontend-wg/issues/103 + +.. _PR Discussion about django settings: https://github.com/openedx/edx-platform/pull/30473#discussion_r916263245 diff --git a/lms/djangoapps/mfe_config_api/tests/__init__.py b/lms/djangoapps/mfe_config_api/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/lms/djangoapps/mfe_config_api/tests/test_views.py b/lms/djangoapps/mfe_config_api/tests/test_views.py new file mode 100644 index 000000000000..35ad520d342e --- /dev/null +++ b/lms/djangoapps/mfe_config_api/tests/test_views.py @@ -0,0 +1,135 @@ +""" +Test the use cases of the views of the mfe api. +""" + +from unittest.mock import call, patch + +import ddt +from django.conf import settings +from django.test import override_settings +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase + + +@ddt.ddt +class MFEConfigTestCase(APITestCase): + """ + Test the use case that exposes the site configuration with the mfe api. + """ + def setUp(self): + self.mfe_config_api_url = reverse("mfe_config_api:config") + return super().setUp() + + @patch("lms.djangoapps.mfe_config_api.views.configuration_helpers") + def test_get_mfe_config(self, configuration_helpers_mock): + """Test the get mfe config from site configuration with the mfe api. + + Expected result: + - The get_value method of the configuration_helpers in the views is called once with the + parameters ("MFE_CONFIG", getattr(settings, "MFE_CONFIG", {})). + - The status of the response of the request is a HTTP_200_OK. + - The json of the response of the request is equal to the mocked configuration. + """ + configuration_helpers_mock.get_value.return_value = {"EXAMPLE_VAR": "value"} + response = self.client.get(self.mfe_config_api_url) + + configuration_helpers_mock.get_value.assert_called_once_with("MFE_CONFIG", getattr(settings, "MFE_CONFIG", {})) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json(), {"EXAMPLE_VAR": "value"}) + + @patch("lms.djangoapps.mfe_config_api.views.configuration_helpers") + def test_get_mfe_config_with_queryparam(self, configuration_helpers_mock): + """Test the get mfe config with a query param from site configuration. + + Expected result: + - The get_value method of the configuration_helpers in the views is called twice, once with the + parameters ("MFE_CONFIG", getattr(settings, "MFE_CONFIG", {})) and once with the parameters + ("MFE_CONFIG_MYMFE", getattr(settings, "MFE_CONFIG_MYMFE", {})). + and one for get_value("MFE_CONFIG_MYMFE", getattr(settings, "MFE_CONFIG_MYMFE", {})). + - The json of the response is the merge of both mocked configurations. + """ + configuration_helpers_mock.get_value.side_effect = [{"EXAMPLE_VAR": "value", "OTHER": "other"}, + {"EXAMPLE_VAR": "mymfe_value"}] + + response = self.client.get(f"{self.mfe_config_api_url}?mfe=mymfe") + self.assertEqual(response.status_code, status.HTTP_200_OK) + calls = [call("MFE_CONFIG", getattr(settings, "MFE_CONFIG", {})), + call("MFE_CONFIG_MYMFE", getattr(settings, "MFE_CONFIG_MYMFE", {}))] + configuration_helpers_mock.get_value.assert_has_calls(calls) + self.assertEqual(response.json(), {"EXAMPLE_VAR": "mymfe_value", "OTHER": "other"}) + + @patch("lms.djangoapps.mfe_config_api.views.configuration_helpers") + @ddt.data( + [{}, {}, {}], + [{"EXAMPLE_VAR": "value"}, {}, {"EXAMPLE_VAR": "value"}], + [{}, {"EXAMPLE_VAR": "mymfe_value"}, {"EXAMPLE_VAR": "mymfe_value"}], + [{"EXAMPLE_VAR": "value"}, {"EXAMPLE_VAR": "mymfe_value"}, {"EXAMPLE_VAR": "mymfe_value"}], + [{"EXAMPLE_VAR": "value", "OTHER": "other"}, {"EXAMPLE_VAR": "mymfe_value"}, + {"EXAMPLE_VAR": "mymfe_value", "OTHER": "other"}], + ) + @ddt.unpack + def test_get_mfe_config_with_queryparam_multiple_configs( + self, + mfe_config, + mfe_config_mymfe, + expected_response, + configuration_helpers_mock + ): + """Test the get mfe config with a query param and different settings in mfe_config and mfe_config_mfe inside + the site configuration to test that the merge of the configurations is done correctly and mymfe config take + precedence. + + In the ddt data the following structure is being passed: + [mfe_config, mfe_config_mymfe, expected_response] + + Expected result: + - The get_value method of the configuration_helpers in the views is called twice, once with the + parameters ("MFE_CONFIG", getattr(settings, "MFE_CONFIG", {})) and once with the parameters + ("MFE_CONFIG_MYMFE", getattr(settings, "MFE_CONFIG_MYMFE", {})). + - The json of the response is the expected_response passed by ddt.data. + """ + configuration_helpers_mock.get_value.side_effect = [mfe_config, mfe_config_mymfe] + + response = self.client.get(f"{self.mfe_config_api_url}?mfe=mymfe") + self.assertEqual(response.status_code, status.HTTP_200_OK) + calls = [call("MFE_CONFIG", getattr(settings, "MFE_CONFIG", {})), + call("MFE_CONFIG_MYMFE", getattr(settings, "MFE_CONFIG_MYMFE", {}))] + configuration_helpers_mock.get_value.assert_has_calls(calls) + self.assertEqual(response.json(), expected_response) + + def test_get_mfe_config_from_django_settings(self): + """Test that when there is no site configuration, the API takes the django settings. + + Expected result: + - The status of the response of the request is a HTTP_200_OK. + - The json response is equal to MFE_CONFIG in lms/envs/test.py""" + response = self.client.get(self.mfe_config_api_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json(), getattr(settings, "MFE_CONFIG", {})) + + def test_get_mfe_config_with_queryparam_from_django_settings(self): + """Test that when there is no site configuration, the API with queryparam takes the django settings. + + Expected result: + - The status of the response of the request is a HTTP_200_OK. + - The json response is equal to MFE_CONFIG merged with MFE_CONFIG_MYMFE in lms/envs/test.py + """ + response = self.client.get(f"{self.mfe_config_api_url}?mfe=mymfe") + self.assertEqual(response.status_code, status.HTTP_200_OK) + expected_response = getattr(settings, "MFE_CONFIG", {}) + expected_response.update(getattr(settings, "MFE_CONFIG_MYMFE", {})) + self.assertEqual(response.json(), expected_response) + + @patch("lms.djangoapps.mfe_config_api.views.configuration_helpers") + @override_settings(ENABLE_MFE_CONFIG_API=False) + def test_404_get_mfe_config(self, configuration_helpers_mock): + """Test the 404 not found response from get mfe config. + + Expected result: + - The get_value method of configuration_helpers is not called. + - The status of the response of the request is a HTTP_404_NOT_FOUND. + """ + response = self.client.get(self.mfe_config_api_url) + configuration_helpers_mock.get_value.assert_not_called() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/lms/djangoapps/mfe_config_api/urls.py b/lms/djangoapps/mfe_config_api/urls.py new file mode 100644 index 000000000000..8f63406a9afd --- /dev/null +++ b/lms/djangoapps/mfe_config_api/urls.py @@ -0,0 +1,10 @@ +""" URLs configuration for the mfe api.""" + +from django.urls import path + +from lms.djangoapps.mfe_config_api.views import MFEConfigView + +app_name = 'mfe_config_api' +urlpatterns = [ + path('', MFEConfigView.as_view(), name='config'), +] diff --git a/lms/djangoapps/mfe_config_api/views.py b/lms/djangoapps/mfe_config_api/views.py new file mode 100644 index 000000000000..13f4e4955c41 --- /dev/null +++ b/lms/djangoapps/mfe_config_api/views.py @@ -0,0 +1,52 @@ +""" +MFE API Views for useful information related to mfes. +""" + +from django.conf import settings +from django.http import HttpResponseNotFound, JsonResponse +from django.utils.decorators import method_decorator +from django.views.decorators.cache import cache_page +from rest_framework import status +from rest_framework.views import APIView + +from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers + + +class MFEConfigView(APIView): + """ + Provides an API endpoint to get the MFE_CONFIG from site configuration. + """ + + @method_decorator(cache_page(settings.MFE_CONFIG_API_CACHE_TIMEOUT)) + def get(self, request): + """ + GET /api/v1/mfe_config + or + GET /api/v1/mfe_config?mfe=name_of_mfe + + **GET Response Values** + ``` + { + "BASE_URL": "https://name_of_mfe.example.com", + "LANGUAGE_PREFERENCE_COOKIE_NAME": "example-language-preference", + "CREDENTIALS_BASE_URL": "https://credentials.example.com", + "DISCOVERY_API_BASE_URL": "https://discovery.example.com", + "LMS_BASE_URL": "https://courses.example.com", + "LOGIN_URL": "https://courses.example.com/login", + "LOGOUT_URL": "https://courses.example.com/logout", + "STUDIO_BASE_URL": "https://studio.example.com", + "LOGO_URL": "https://courses.example.com/logo.png" + } + ``` + """ + + if not settings.ENABLE_MFE_CONFIG_API: + return HttpResponseNotFound() + + mfe_config = configuration_helpers.get_value('MFE_CONFIG', getattr(settings, 'MFE_CONFIG', {})) + if request.query_params.get('mfe'): + mfe = str(request.query_params.get('mfe')).upper() + mfe_config.update(configuration_helpers.get_value( + f'MFE_CONFIG_{mfe}', getattr(settings, f'MFE_CONFIG_{mfe}', {}))) + + return JsonResponse(mfe_config, status=status.HTTP_200_OK) diff --git a/lms/envs/common.py b/lms/envs/common.py index 431a8bc3bb43..267edc04664e 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3261,6 +3261,9 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring # Blockstore 'blockstore.apps.bundles', + + # MFE API + 'lms.djangoapps.mfe_config_api', ] ######################### CSRF ######################################### @@ -5134,3 +5137,39 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring "duplicate": _("Post is a duplicate"), "off-topic": _("Post is off-topic"), } +# .. toggle_name: ENABLE_MFE_CONFIG_API +# .. toggle_implementation: DjangoSetting +# .. toggle_default: False +# .. toggle_description: Set to True to enable MFE Config API. This is disabled by +# default. +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2022-05-20 +# .. toggle_target_removal_date: None +# .. toggle_warnings: None +# .. toggle_tickets: None +ENABLE_MFE_CONFIG_API = False + +# .. setting_name: MFE_CONFIG +# .. setting_implementation: DjangoSetting +# .. setting_default: {} +# .. setting_description: Is a configuration that will be exposed by the MFE Config API to be consumed by the mfes +# Example: { +# "BASE_URL": "https://name_of_mfe.example.com", +# "LANGUAGE_PREFERENCE_COOKIE_NAME": "example-language-preference", +# "CREDENTIALS_BASE_URL": "https://credentials.example.com", +# "DISCOVERY_API_BASE_URL": "https://discovery.example.com", +# "LMS_BASE_URL": "https://courses.example.com", +# "LOGIN_URL": "https://courses.example.com/login", +# "LOGOUT_URL": "https://courses.example.com/logout", +# "STUDIO_BASE_URL": "https://studio.example.com", +# "LOGO_URL": "https://courses.example.com/logo.png" +# } +# .. setting_use_cases: open_edx +# .. setting_creation_date: 2022-07-08 +MFE_CONFIG = {} + +# .. setting_name: MFE_CONFIG_API_CACHE_TIMEOUT +# .. setting_default: 60*5 +# .. setting_description: The MFE Config API response will be cached during the +# specified time +MFE_CONFIG_API_CACHE_TIMEOUT = 60 * 5 diff --git a/lms/envs/test.py b/lms/envs/test.py index b50f3e5e8a05..6f2045f0071b 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -636,3 +636,16 @@ #################### Network configuration #################### # Tests are not behind any proxies CLOSEST_CLIENT_IP_FROM_HEADERS = [] + +################## MFE API #################### +ENABLE_MFE_CONFIG_API = True +MFE_CONFIG = { + "BASE_URL": "https://name_of_mfe.example.com", + "LANGUAGE_PREFERENCE_COOKIE_NAME": "example-language-preference", + "LOGO_URL": "https://courses.example.com/logo.png" +} + +MFE_CONFIG_MYMFE = { + "LANGUAGE_PREFERENCE_COOKIE_NAME": "mymfe-language-preference", + "LOGO_URL": "https://courses.example.com/mymfe-logo.png" +} diff --git a/lms/urls.py b/lms/urls.py index 11e6913975ae..f1b73d5dab28 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -1040,3 +1040,8 @@ urlpatterns += [ path('api/ora_staff_grader/', include('lms.djangoapps.ora_staff_grader.urls', 'ora-staff-grader')), ] + +# MFE API urls +urlpatterns += [ + path('api/v1/mfe_config', include(('lms.djangoapps.mfe_config_api.urls', 'lms.djangoapps.mfe_config_api'), namespace='mfe_config_api')) +] From 97bf612aa703c547e4b8c8a504d30870b49df0df Mon Sep 17 00:00:00 2001 From: Kyle McCormick Date: Fri, 5 Aug 2022 16:34:00 -0400 Subject: [PATCH 3/5] feat!: change names of dynamic MFE config Django settings Formerly, the settings were: * `MFE_CONFIG` for common config. * `MFE_CONFIG_` for app-specific overrides, with each app getting its own Django setting. This commit changes it to: * `MFE_CONFIG` for common config (unchanged) * `MFE_CONFIG_OVERRIDES` for app-specific overrides, where each app gets a top-level key in the dictionary. Why the change? * We want common.py to have a complete list of overridable settings, as it helps operators reason about configuration and allows us to generate config documentation using toggle annotations. Dynamically generating setting names based on arbitrary APP_IDs makes this impossible. * getattr(...) generally makes code more complicated bug prone. Tools like pylint and mypy cannot effectively analyze any code that uses dynamic attribute access. (cherry picked from commit 8edefe74ff536c0c8084d7232e6b1b0367999606) --- .../docs/decisions/0001-mfe-config-api.rst | 2 +- .../mfe_config_api/tests/test_views.py | 95 ++++++++++++------- lms/djangoapps/mfe_config_api/views.py | 12 ++- lms/envs/common.py | 29 +++++- lms/envs/test.py | 12 ++- 5 files changed, 104 insertions(+), 46 deletions(-) diff --git a/lms/djangoapps/mfe_config_api/docs/decisions/0001-mfe-config-api.rst b/lms/djangoapps/mfe_config_api/docs/decisions/0001-mfe-config-api.rst index d26a40dc9ae6..f9bd7e636170 100644 --- a/lms/djangoapps/mfe_config_api/docs/decisions/0001-mfe-config-api.rst +++ b/lms/djangoapps/mfe_config_api/docs/decisions/0001-mfe-config-api.rst @@ -18,7 +18,7 @@ Decision - A lightweight API will be created that returns the mfe configuration variables from the site configuration or django settings. `PR Discussion about django settings`_ - The API will be enabled or disabled using the setting ``ENABLE_MFE_CONFIG_API``. - The API will take the mfe configuration in the ``MFE_CONFIG`` keyset in the site configuration (admin > site configuration > your domain) or in django settings. -- This API allows to consult the configurations by specific MFE. Making a request like ``api/v1/mfe_config?mfe=mymfe`` will return the configuration defined in ``MFE_CONFIG_MYMFE`` merged with the ``MFE_CONFIG`` configuration. +- This API allows to consult the configurations by specific MFE. Making a request like ``/api/v1/mfe_config?mfe=mymfe`` will return the configuration defined in ``MFE_CONFIG_OVERRIDES["mymfe"]`` merged with the ``MFE_CONFIG`` configuration. - The API will have a mechanism to cache the response with ``MFE_CONFIG_API_CACHE_TIMEOUT`` variable. - The API will live in lms/djangoapps because this is not something Studio needs to serve and it is a lightweight API. `PR Discussion`_ - The API will not require authentication or authorization. diff --git a/lms/djangoapps/mfe_config_api/tests/test_views.py b/lms/djangoapps/mfe_config_api/tests/test_views.py index 35ad520d342e..fef7574f5932 100644 --- a/lms/djangoapps/mfe_config_api/tests/test_views.py +++ b/lms/djangoapps/mfe_config_api/tests/test_views.py @@ -27,14 +27,14 @@ def test_get_mfe_config(self, configuration_helpers_mock): Expected result: - The get_value method of the configuration_helpers in the views is called once with the - parameters ("MFE_CONFIG", getattr(settings, "MFE_CONFIG", {})). + parameters ("MFE_CONFIG", settings.MFE_CONFIG) - The status of the response of the request is a HTTP_200_OK. - The json of the response of the request is equal to the mocked configuration. """ configuration_helpers_mock.get_value.return_value = {"EXAMPLE_VAR": "value"} response = self.client.get(self.mfe_config_api_url) - configuration_helpers_mock.get_value.assert_called_once_with("MFE_CONFIG", getattr(settings, "MFE_CONFIG", {})) + configuration_helpers_mock.get_value.assert_called_once_with("MFE_CONFIG", settings.MFE_CONFIG) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.json(), {"EXAMPLE_VAR": "value"}) @@ -44,57 +44,87 @@ def test_get_mfe_config_with_queryparam(self, configuration_helpers_mock): Expected result: - The get_value method of the configuration_helpers in the views is called twice, once with the - parameters ("MFE_CONFIG", getattr(settings, "MFE_CONFIG", {})) and once with the parameters - ("MFE_CONFIG_MYMFE", getattr(settings, "MFE_CONFIG_MYMFE", {})). - and one for get_value("MFE_CONFIG_MYMFE", getattr(settings, "MFE_CONFIG_MYMFE", {})). + parameters ("MFE_CONFIG", settings.MFE_CONFIG) + and once with the parameters ("MFE_CONFIG_OVERRIDES", settings.MFE_CONFIG_OVERRIDES). - The json of the response is the merge of both mocked configurations. """ - configuration_helpers_mock.get_value.side_effect = [{"EXAMPLE_VAR": "value", "OTHER": "other"}, - {"EXAMPLE_VAR": "mymfe_value"}] + configuration_helpers_mock.get_value.side_effect = [ + {"EXAMPLE_VAR": "value", "OTHER": "other"}, + {"mymfe": {"EXAMPLE_VAR": "mymfe_value"}}, + ] response = self.client.get(f"{self.mfe_config_api_url}?mfe=mymfe") self.assertEqual(response.status_code, status.HTTP_200_OK) - calls = [call("MFE_CONFIG", getattr(settings, "MFE_CONFIG", {})), - call("MFE_CONFIG_MYMFE", getattr(settings, "MFE_CONFIG_MYMFE", {}))] + calls = [call("MFE_CONFIG", settings.MFE_CONFIG), + call("MFE_CONFIG_OVERRIDES", settings.MFE_CONFIG_OVERRIDES)] configuration_helpers_mock.get_value.assert_has_calls(calls) self.assertEqual(response.json(), {"EXAMPLE_VAR": "mymfe_value", "OTHER": "other"}) - @patch("lms.djangoapps.mfe_config_api.views.configuration_helpers") + @ddt.unpack @ddt.data( - [{}, {}, {}], - [{"EXAMPLE_VAR": "value"}, {}, {"EXAMPLE_VAR": "value"}], - [{}, {"EXAMPLE_VAR": "mymfe_value"}, {"EXAMPLE_VAR": "mymfe_value"}], - [{"EXAMPLE_VAR": "value"}, {"EXAMPLE_VAR": "mymfe_value"}, {"EXAMPLE_VAR": "mymfe_value"}], - [{"EXAMPLE_VAR": "value", "OTHER": "other"}, {"EXAMPLE_VAR": "mymfe_value"}, - {"EXAMPLE_VAR": "mymfe_value", "OTHER": "other"}], + dict( + mfe_config={}, + mfe_config_overrides={}, + expected_response={}, + ), + dict( + mfe_config={"EXAMPLE_VAR": "value"}, + mfe_config_overrides={}, + expected_response={"EXAMPLE_VAR": "value"}, + ), + dict( + mfe_config={}, + mfe_config_overrides={"mymfe": {"EXAMPLE_VAR": "mymfe_value"}}, + expected_response={"EXAMPLE_VAR": "mymfe_value"}, + ), + dict( + mfe_config={"EXAMPLE_VAR": "value"}, + mfe_config_overrides={"mymfe": {"EXAMPLE_VAR": "mymfe_value"}}, + expected_response={"EXAMPLE_VAR": "mymfe_value"}, + ), + dict( + mfe_config={"EXAMPLE_VAR": "value", "OTHER": "other"}, + mfe_config_overrides={"mymfe": {"EXAMPLE_VAR": "mymfe_value"}}, + expected_response={"EXAMPLE_VAR": "mymfe_value", "OTHER": "other"}, + ), + dict( + mfe_config={"EXAMPLE_VAR": "value"}, + mfe_config_overrides={"yourmfe": {"EXAMPLE_VAR": "yourmfe_value"}}, + expected_response={"EXAMPLE_VAR": "value"}, + ), + dict( + mfe_config={"EXAMPLE_VAR": "value"}, + mfe_config_overrides={ + "yourmfe": {"EXAMPLE_VAR": "yourmfe_value"}, + "mymfe": {"EXAMPLE_VAR": "mymfe_value"}, + }, + expected_response={"EXAMPLE_VAR": "mymfe_value"}, + ), ) - @ddt.unpack + @patch("lms.djangoapps.mfe_config_api.views.configuration_helpers") def test_get_mfe_config_with_queryparam_multiple_configs( self, + configuration_helpers_mock, mfe_config, - mfe_config_mymfe, + mfe_config_overrides, expected_response, - configuration_helpers_mock ): - """Test the get mfe config with a query param and different settings in mfe_config and mfe_config_mfe inside + """Test the get mfe config with a query param and different settings in mfe_config and mfe_config_overrides with the site configuration to test that the merge of the configurations is done correctly and mymfe config take precedence. - In the ddt data the following structure is being passed: - [mfe_config, mfe_config_mymfe, expected_response] - Expected result: - The get_value method of the configuration_helpers in the views is called twice, once with the - parameters ("MFE_CONFIG", getattr(settings, "MFE_CONFIG", {})) and once with the parameters - ("MFE_CONFIG_MYMFE", getattr(settings, "MFE_CONFIG_MYMFE", {})). + parameters ("MFE_CONFIG", settings.MFE_CONFIG) + and once with the parameters ("MFE_CONFIG_OVERRIDES", settings.MFE_CONFIG_OVERRIDES). - The json of the response is the expected_response passed by ddt.data. """ - configuration_helpers_mock.get_value.side_effect = [mfe_config, mfe_config_mymfe] + configuration_helpers_mock.get_value.side_effect = [mfe_config, mfe_config_overrides] response = self.client.get(f"{self.mfe_config_api_url}?mfe=mymfe") self.assertEqual(response.status_code, status.HTTP_200_OK) - calls = [call("MFE_CONFIG", getattr(settings, "MFE_CONFIG", {})), - call("MFE_CONFIG_MYMFE", getattr(settings, "MFE_CONFIG_MYMFE", {}))] + calls = [call("MFE_CONFIG", settings.MFE_CONFIG), + call("MFE_CONFIG_OVERRIDES", settings.MFE_CONFIG_OVERRIDES)] configuration_helpers_mock.get_value.assert_has_calls(calls) self.assertEqual(response.json(), expected_response) @@ -106,20 +136,19 @@ def test_get_mfe_config_from_django_settings(self): - The json response is equal to MFE_CONFIG in lms/envs/test.py""" response = self.client.get(self.mfe_config_api_url) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json(), getattr(settings, "MFE_CONFIG", {})) + self.assertEqual(response.json(), settings.MFE_CONFIG) def test_get_mfe_config_with_queryparam_from_django_settings(self): """Test that when there is no site configuration, the API with queryparam takes the django settings. Expected result: - The status of the response of the request is a HTTP_200_OK. - - The json response is equal to MFE_CONFIG merged with MFE_CONFIG_MYMFE in lms/envs/test.py + - The json response is equal to MFE_CONFIG merged with MFE_CONFIG_OVERRIDES['mymfe'] """ response = self.client.get(f"{self.mfe_config_api_url}?mfe=mymfe") self.assertEqual(response.status_code, status.HTTP_200_OK) - expected_response = getattr(settings, "MFE_CONFIG", {}) - expected_response.update(getattr(settings, "MFE_CONFIG_MYMFE", {})) - self.assertEqual(response.json(), expected_response) + expected = {**settings.MFE_CONFIG, **settings.MFE_CONFIG_OVERRIDES["mymfe"]} + self.assertEqual(response.json(), expected) @patch("lms.djangoapps.mfe_config_api.views.configuration_helpers") @override_settings(ENABLE_MFE_CONFIG_API=False) diff --git a/lms/djangoapps/mfe_config_api/views.py b/lms/djangoapps/mfe_config_api/views.py index 13f4e4955c41..42a5d603384e 100644 --- a/lms/djangoapps/mfe_config_api/views.py +++ b/lms/djangoapps/mfe_config_api/views.py @@ -43,10 +43,12 @@ def get(self, request): if not settings.ENABLE_MFE_CONFIG_API: return HttpResponseNotFound() - mfe_config = configuration_helpers.get_value('MFE_CONFIG', getattr(settings, 'MFE_CONFIG', {})) + mfe_config = configuration_helpers.get_value('MFE_CONFIG', settings.MFE_CONFIG) if request.query_params.get('mfe'): - mfe = str(request.query_params.get('mfe')).upper() - mfe_config.update(configuration_helpers.get_value( - f'MFE_CONFIG_{mfe}', getattr(settings, f'MFE_CONFIG_{mfe}', {}))) - + mfe = str(request.query_params.get('mfe')) + app_config = configuration_helpers.get_value( + 'MFE_CONFIG_OVERRIDES', + settings.MFE_CONFIG_OVERRIDES, + ) + mfe_config.update(app_config.get(mfe, {})) return JsonResponse(mfe_config, status=status.HTTP_200_OK) diff --git a/lms/envs/common.py b/lms/envs/common.py index 267edc04664e..a63fba80648d 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -5152,8 +5152,10 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring # .. setting_name: MFE_CONFIG # .. setting_implementation: DjangoSetting # .. setting_default: {} -# .. setting_description: Is a configuration that will be exposed by the MFE Config API to be consumed by the mfes -# Example: { +# .. setting_description: Is a configuration that will be exposed by the MFE Config API to be consumed by the MFEs. +# Contains configuration common to all MFEs. When a specific MFE's configuration is requested, these values +# will be treated as a base and then overriden/supplemented by those in `MFE_CONFIG_OVERRIDES`. +# Example: { # "BASE_URL": "https://name_of_mfe.example.com", # "LANGUAGE_PREFERENCE_COOKIE_NAME": "example-language-preference", # "CREDENTIALS_BASE_URL": "https://credentials.example.com", @@ -5163,11 +5165,30 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring # "LOGOUT_URL": "https://courses.example.com/logout", # "STUDIO_BASE_URL": "https://studio.example.com", # "LOGO_URL": "https://courses.example.com/logo.png" -# } +# } # .. setting_use_cases: open_edx -# .. setting_creation_date: 2022-07-08 +# .. setting_creation_date: 2022-08-05 MFE_CONFIG = {} +# .. setting_name: MFE_CONFIG_OVERRIDES +# .. setting_implementation: DjangoSetting +# .. setting_default: {} +# .. setting_description: Overrides or additions to `MFE_CONFIG` for when a specific MFE is requested +# by the MFE Config API. Top-level keys are APP_IDs, a.k.a. the name of the MFE (for example, +# for an MFE named "frontend-app-xyz", the top-level key would be "xyz"). +# Example: { +# "gradebook": { +# "BASE_URL": "https://gradebook.example.com", +# }, +# "profile": { +# "BASE_URL": "https://profile.example.com", +# "ENABLE_LEARNER_RECORD_MFE": "true", +# }, +# } +# .. setting_use_cases: open_edx +# .. setting_creation_date: 2022-08-05 +MFE_CONFIG_OVERRIDES = {} + # .. setting_name: MFE_CONFIG_API_CACHE_TIMEOUT # .. setting_default: 60*5 # .. setting_description: The MFE Config API response will be cached during the diff --git a/lms/envs/test.py b/lms/envs/test.py index 6f2045f0071b..b84abdbc9ddc 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -645,7 +645,13 @@ "LOGO_URL": "https://courses.example.com/logo.png" } -MFE_CONFIG_MYMFE = { - "LANGUAGE_PREFERENCE_COOKIE_NAME": "mymfe-language-preference", - "LOGO_URL": "https://courses.example.com/mymfe-logo.png" +MFE_CONFIG_OVERRIDES = { + "mymfe": { + "LANGUAGE_PREFERENCE_COOKIE_NAME": "mymfe-language-preference", + "LOGO_URL": "https://courses.example.com/mymfe-logo.png", + }, + "yourmfe": { + "LANGUAGE_PREFERENCE_COOKIE_NAME": "yourmfe-language-preference", + "LOGO_URL": "https://courses.example.com/yourmfe-logo.png", + }, } From 1040fd1439301bdba3dcd2f9a979da599e558765 Mon Sep 17 00:00:00 2001 From: Kyle McCormick Date: Fri, 5 Aug 2022 16:51:47 -0400 Subject: [PATCH 4/5] docs: add more detail to MFE Config API documentation No functional changes here. This just uses the edx_api_doc_tools package to add some additional documentation to the new API. The documentation can be read from the code, or viewed by visiting http:///api-docs and searching for "mfe_config". (cherry picked from commit 1b52ad58a53289f316a394469686a2b939e36e69) --- lms/djangoapps/mfe_config_api/views.py | 27 +++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/mfe_config_api/views.py b/lms/djangoapps/mfe_config_api/views.py index 42a5d603384e..c124059406bb 100644 --- a/lms/djangoapps/mfe_config_api/views.py +++ b/lms/djangoapps/mfe_config_api/views.py @@ -2,6 +2,7 @@ MFE API Views for useful information related to mfes. """ +import edx_api_doc_tools as apidocs from django.conf import settings from django.http import HttpResponseNotFound, JsonResponse from django.utils.decorators import method_decorator @@ -14,15 +15,30 @@ class MFEConfigView(APIView): """ - Provides an API endpoint to get the MFE_CONFIG from site configuration. + Provides an API endpoint to get the MFE configuration from settings (or site configuration). """ @method_decorator(cache_page(settings.MFE_CONFIG_API_CACHE_TIMEOUT)) + @apidocs.schema( + parameters=[ + apidocs.query_parameter( + 'mfe', + str, + description="Name of an MFE (a.k.a. an APP_ID).", + ), + ], + ) def get(self, request): """ - GET /api/v1/mfe_config - or - GET /api/v1/mfe_config?mfe=name_of_mfe + Return the MFE configuration, optionally including MFE-specific overrides. + + **Usage** + + Get common config: + GET /api/v1/mfe_config + + Get app config (common + app-specific overrides): + GET /api/v1/mfe_config?mfe=name_of_mfe **GET Response Values** ``` @@ -35,7 +51,8 @@ def get(self, request): "LOGIN_URL": "https://courses.example.com/login", "LOGOUT_URL": "https://courses.example.com/logout", "STUDIO_BASE_URL": "https://studio.example.com", - "LOGO_URL": "https://courses.example.com/logo.png" + "LOGO_URL": "https://courses.example.com/logo.png", + ... and so on } ``` """ From ee49bf49ab486ba9fb9cf9435de66faf52e9206b Mon Sep 17 00:00:00 2001 From: Kyle McCormick Date: Fri, 5 Aug 2022 17:12:30 -0400 Subject: [PATCH 5/5] feat!: change /api/v1/mfe_config to /api/mfe_config/v1 * This changes the API's path. The reasoning is that this is Version 1 of the mfe_config API, not Version 1 of the LMS's entire API, so the v1 should come after mfe_config. * Why does this matter? Firstly, consistency. Secondly, it affects our generated API documentation. If you visited https://courses.edx.org/api-docs, you could see that the API was listed under "v1" instead of "mfe_config". (cherry picked from commit c253ec418100b5b1980040dda89e60e1b7515581) --- .../mfe_config_api/docs/decisions/0001-mfe-config-api.rst | 4 ++-- lms/djangoapps/mfe_config_api/views.py | 4 ++-- lms/urls.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/mfe_config_api/docs/decisions/0001-mfe-config-api.rst b/lms/djangoapps/mfe_config_api/docs/decisions/0001-mfe-config-api.rst index f9bd7e636170..8e298991186d 100644 --- a/lms/djangoapps/mfe_config_api/docs/decisions/0001-mfe-config-api.rst +++ b/lms/djangoapps/mfe_config_api/docs/decisions/0001-mfe-config-api.rst @@ -18,7 +18,7 @@ Decision - A lightweight API will be created that returns the mfe configuration variables from the site configuration or django settings. `PR Discussion about django settings`_ - The API will be enabled or disabled using the setting ``ENABLE_MFE_CONFIG_API``. - The API will take the mfe configuration in the ``MFE_CONFIG`` keyset in the site configuration (admin > site configuration > your domain) or in django settings. -- This API allows to consult the configurations by specific MFE. Making a request like ``/api/v1/mfe_config?mfe=mymfe`` will return the configuration defined in ``MFE_CONFIG_OVERRIDES["mymfe"]`` merged with the ``MFE_CONFIG`` configuration. +- This API allows to consult the configurations by specific MFE. Making a request like ``/api/mfe_config/v1?mfe=mymfe`` will return the configuration defined in ``MFE_CONFIG_OVERRIDES["mymfe"]`` merged with the ``MFE_CONFIG`` configuration. - The API will have a mechanism to cache the response with ``MFE_CONFIG_API_CACHE_TIMEOUT`` variable. - The API will live in lms/djangoapps because this is not something Studio needs to serve and it is a lightweight API. `PR Discussion`_ - The API will not require authentication or authorization. @@ -26,7 +26,7 @@ Decision Request:: - GET http://lms.base.com/api/v1/mfe_config?mfe=learning + GET http://lms.base.com/api/mfe_config/v1?mfe=learning Response:: diff --git a/lms/djangoapps/mfe_config_api/views.py b/lms/djangoapps/mfe_config_api/views.py index c124059406bb..da875393dc68 100644 --- a/lms/djangoapps/mfe_config_api/views.py +++ b/lms/djangoapps/mfe_config_api/views.py @@ -35,10 +35,10 @@ def get(self, request): **Usage** Get common config: - GET /api/v1/mfe_config + GET /api/mfe_config/v1 Get app config (common + app-specific overrides): - GET /api/v1/mfe_config?mfe=name_of_mfe + GET /api/mfe_config/v1?mfe=name_of_mfe **GET Response Values** ``` diff --git a/lms/urls.py b/lms/urls.py index f1b73d5dab28..2485cbce3f7e 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -1043,5 +1043,5 @@ # MFE API urls urlpatterns += [ - path('api/v1/mfe_config', include(('lms.djangoapps.mfe_config_api.urls', 'lms.djangoapps.mfe_config_api'), namespace='mfe_config_api')) + path('api/mfe_config/v1', include(('lms.djangoapps.mfe_config_api.urls', 'lms.djangoapps.mfe_config_api'), namespace='mfe_config_api')) ]