From 7f5eb296a3a8140981cb36f4b1606e29b722f6c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Fernanda=20Magallanes=20Z?= Date: Fri, 20 May 2022 13:48:59 -0400 Subject: [PATCH] feat: add mfe_api v1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds MFE API. This is part of the work that is being done to obtain the MFE Runtime Configurations and that has been discussed in the BTR WG. Discussion: https://discuss.openedx.org/t/how-to-use-microfrontend-in-a-multitenant-instance/6936/14?u=mafermazu MFE Runtime configuration - eduNEXT: https://docs.google.com/document/d/1-FHIQmyeQZu3311x8eYUNMru4JX7Yb3UlqjmJxvM8do/edit?usp=sharing feat: add lms setting to set mfe config cache (#262) Co-authored-by: MarĂ­a Fernanda Magallanes Z feat: make mfe config api disabled by default (#263) * feat: make mfe config api disabled by default * fix: simple is better than complex test: add mfe config tests (#264) * test: add mfe config tests * test: fix it and simplify it * test: correct pylint issues fix: correct pep 8 violations fix: add mfe api unit test in github workflow fix: correct unit tests refactor: move mfe api to lms fix: try mfe api urls without regex fix: add app_namespace in lms urls fix: try url without conditional Revert "fix: try url without conditional" This reverts commit 694aab546134b4bd9ad2642e24927b42cac24459. fix: set enable_mfe_config_api feature to true in the tests test: try to add failed test case Revert "test: try to add failed test case" This reverts commit cee6bf656ab1b96492b0b6199ddff32a6d6a65bd. docs: improve explanation and documentation fix: ensure the response is a json object refactor: be consistent with the variable names fix: allow overriding mfe api config cache timeout in production fix: handle 404 response in view refactor: use a guard instead if-else feat: add the possibility to show mfe specific config --- .github/workflows/pylint-checks.yml | 2 +- .github/workflows/unit-test-shards.json | 1 + lms/djangoapps/mfe_api/__init__.py | 0 lms/djangoapps/mfe_api/tests/__init__.py | 0 lms/djangoapps/mfe_api/tests/test_views.py | 43 ++++++++++++++++++++++ lms/djangoapps/mfe_api/urls.py | 10 +++++ lms/djangoapps/mfe_api/views.py | 42 +++++++++++++++++++++ lms/envs/common.py | 21 +++++++++++ lms/envs/production.py | 3 ++ lms/envs/test.py | 3 ++ lms/urls.py | 5 +++ 11 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 lms/djangoapps/mfe_api/__init__.py create mode 100644 lms/djangoapps/mfe_api/tests/__init__.py create mode 100644 lms/djangoapps/mfe_api/tests/test_views.py create mode 100644 lms/djangoapps/mfe_api/urls.py create mode 100644 lms/djangoapps/mfe_api/views.py diff --git a/.github/workflows/pylint-checks.yml b/.github/workflows/pylint-checks.yml index f1e3d02bba..43677f6bc1 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_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 15d5ce270e..c63ce0f97c 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_api/", "lms/envs/", "lms/lib/", "lms/tests.py" diff --git a/lms/djangoapps/mfe_api/__init__.py b/lms/djangoapps/mfe_api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/mfe_api/tests/__init__.py b/lms/djangoapps/mfe_api/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/mfe_api/tests/test_views.py b/lms/djangoapps/mfe_api/tests/test_views.py new file mode 100644 index 0000000000..2b28bd87fc --- /dev/null +++ b/lms/djangoapps/mfe_api/tests/test_views.py @@ -0,0 +1,43 @@ +""" +Test the use cases of the views of the mfe api. +""" + +from unittest.mock import patch + +from django.conf import settings +from django.urls import reverse +from rest_framework import status + +from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers +from openedx.core.lib.api.test_utils import ApiTestCase + + +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_api:config') + return super().setUp() + + def test_get_mfe_config(self): + """Test the get mfe config from site configuration with the mfe api. + + Expected result: + - Inside self.get_json pass the response is a json and the status is 200 asserts. + - The configuration obtained by the api is equal to its site configuration in the + MFE_CONFIG key. + """ + mfe_config = configuration_helpers.get_value('MFE_CONFIG', {}) + response_json = self.get_json(self.mfe_config_api_url) + assert response_json == mfe_config + + @patch.dict(settings.FEATURES, {'ENABLE_MFE_API': False}) + def test_404_get_mfe_config(self): + """Test the 404 not found response from get mfe config. + + Expected result: + - Response status code equal to 404 + """ + response = self.client.get(self.mfe_config_api_url) + assert response.status_code == status.HTTP_404_NOT_FOUND diff --git a/lms/djangoapps/mfe_api/urls.py b/lms/djangoapps/mfe_api/urls.py new file mode 100644 index 0000000000..5cc5152345 --- /dev/null +++ b/lms/djangoapps/mfe_api/urls.py @@ -0,0 +1,10 @@ +""" URLs configuration for the mfe api.""" + +from django.urls import path + +from lms.djangoapps.mfe_api.views import MFEConfigView + +app_name = 'mfe_api' +urlpatterns = [ + path('v1/config', MFEConfigView.as_view(), name='config'), +] diff --git a/lms/djangoapps/mfe_api/views.py b/lms/djangoapps/mfe_api/views.py new file mode 100644 index 0000000000..40764b4e6c --- /dev/null +++ b/lms/djangoapps/mfe_api/views.py @@ -0,0 +1,42 @@ +""" +MFE API Views for useful information related to mfes. +""" + +from django.conf import settings +from django.http import 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_API_CONFIG_CACHE_TIMEOUT)) + def get(self, request): + """ + GET /api/mfe/v1/config + + **GET Response Values** + ``` + { + "LOGO_URL": "https://example.com/logo.png", + } + ``` + """ + + if not settings.FEATURES.get('ENABLE_MFE_API'): + msg = 'MFE API not found. Try setting FEATURES["ENABLE_MFE_API"] to true.' + return JsonResponse({'message': msg}, status=status.HTTP_404_NOT_FOUND) + + mfe_config = {'MFE_CONFIG': configuration_helpers.get_value('MFE_CONFIG', {})} + if request.query_params.get('mfe'): + mfe = str(request.query_params.get('mfe')).upper() + mfe_config[f'MFE_CONFIG_{mfe}']= configuration_helpers.get_value(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 d61433ed9e..0934236dc1 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1018,6 +1018,18 @@ FEATURES = { # .. toggle_target_removal_date: None # .. toggle_tickets: 'https://openedx.atlassian.net/browse/MST-1458' 'ENABLE_CERTIFICATES_IDV_REQUIREMENT': False, + + # .. toggle_name: FEATURES['ENABLE_MFE_API'] + # .. toggle_implementation: DjangoSetting + # .. toggle_default: False + # .. toggle_description: Set to True to enable MFE Config REST 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_API': False, } # Specifies extra XBlock fields that should available when requested via the Course Blocks API @@ -3251,6 +3263,9 @@ INSTALLED_APPS = [ # Blockstore 'blockstore.apps.bundles', + + # MFE API + 'lms.djangoapps.mfe_api', ] ######################### CSRF ######################################### @@ -4909,6 +4924,12 @@ HIBP_LOGIN_BLOCK_PASSWORD_FREQUENCY_THRESHOLD = 5 # .. toggle_tickets: https://openedx.atlassian.net/browse/VAN-838 ENABLE_DYNAMIC_REGISTRATION_FIELDS = False +# .. setting_name: MFE_API_CONFIG_CACHE_TIMEOUT +# .. setting_default: 60*5 +# .. setting_description: The MFE_CONFIG site configuration will be cached during the +# specified time +MFE_API_CONFIG_CACHE_TIMEOUT = 60 * 5 + ############### Settings for the ace_common plugin ################# # Note that all settings are actually defined by the plugin # pylint: disable=wrong-import-position diff --git a/lms/envs/production.py b/lms/envs/production.py index fabcc024ef..b1b0b6bc6e 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1078,3 +1078,6 @@ COURSE_LIVE_GLOBAL_CREDENTIALS["BIG_BLUE_BUTTON"] = { "SECRET": ENV_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_SECRET', None), "URL": ENV_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_URL', None), } + +############################# MFE API CONFIG CACHE TIMEOUT############################ +MFE_API_CONFIG_CACHE_TIMEOUT = ENV_TOKENS.get('MFE_API_CONFIG_CACHE_TIMEOUT', MFE_API_CONFIG_CACHE_TIMEOUT) diff --git a/lms/envs/test.py b/lms/envs/test.py index edef277574..4186d98e45 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -647,3 +647,6 @@ COURSE_LIVE_GLOBAL_CREDENTIALS["BIG_BLUE_BUTTON"] = { "SECRET": "***", "URL": "***", } + +################## MFE API #################### +FEATURES['ENABLE_MFE_API'] = True diff --git a/lms/urls.py b/lms/urls.py index 352a25dee5..974e14bc27 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -1022,3 +1022,8 @@ urlpatterns += [ urlpatterns += [ path('api/instructor_task/', include('lms.djangoapps.instructor_task.rest_api.urls')), ] + +# MFE API urls +urlpatterns += [ + path('api/mfe/', include(('lms.djangoapps.mfe_api.urls', 'lms.djangoapps.mfe_api'), namespace='mfe_api')) +]