diff --git a/cms/envs/common.py b/cms/envs/common.py index d85a10222bfa..bde70b8583d1 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -2453,6 +2453,17 @@ SHOW_ACTIVATE_CTA_POPUP_COOKIE_NAME = 'show-account-activation-popup' SHOW_ACCOUNT_ACTIVATION_CTA = False +############# Shorten MFE URL ########################## +# .. toggle_name: enable_short_mfe_url +# .. toggle_implementation: DjangoSetting +# .. toggle_default: False +# .. toggle_description: Flag would be used to hash block_ids and shorten the MFE url +# .. toggle_use_cases: temporary +# .. toggle_tickets: https://github.com/edx/edx-platform/pull/28174/ +# .. toggle_creation_date: 2021-08-03 +# .. toggle_target_removal_date: 2022-01-01 +ENABLE_SHORT_MFE_URL = False + ################# Documentation links for course apps ################# # pylint: disable=line-too-long diff --git a/lms/djangoapps/course_api/blocks/views.py b/lms/djangoapps/course_api/blocks/views.py index 6aa480218d46..f3ee306dd6d0 100644 --- a/lms/djangoapps/course_api/blocks/views.py +++ b/lms/djangoapps/course_api/blocks/views.py @@ -2,7 +2,6 @@ CourseBlocks API views """ - from django.core.exceptions import ValidationError from django.db import transaction from django.http import Http404 @@ -14,6 +13,7 @@ from rest_framework.response import Response from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes +from openedx.features.course_experience.url_helpers import get_usage_key_hash from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError @@ -240,6 +240,17 @@ def list(self, request, usage_key_string, hide_access_denials=False): # pylint: # case we add the usual caching headers to the response. if params.cleaned_data.get('username', None) == '': patch_response_headers(response) + + if 'blocks' in response.data: + blocks = response.data['blocks'] + else: + blocks = response.data + + for block in blocks: + current_block = block + if isinstance(blocks, dict): + current_block = blocks[block] + current_block['hash_key'] = get_usage_key_hash(current_block['id']) return response except ItemNotFoundError as exception: raise Http404(f"Block not found: {str(exception)}") # lint-amnesty, pylint: disable=raise-missing-from diff --git a/lms/djangoapps/course_home_api/outline/v1/serializers.py b/lms/djangoapps/course_home_api/outline/v1/serializers.py index 1cab4cd27fdd..11af8a5f299f 100644 --- a/lms/djangoapps/course_home_api/outline/v1/serializers.py +++ b/lms/djangoapps/course_home_api/outline/v1/serializers.py @@ -3,6 +3,7 @@ """ from django.utils.translation import ngettext +from django.conf import settings from rest_framework import serializers from lms.djangoapps.course_home_api.dates.v1.serializers import DateSummarySerializer @@ -27,6 +28,7 @@ def get_blocks(self, block): icon = None num_graded_problems = block.get('num_graded_problems', 0) scored = block.get('scored') + hash_key = block['hash_key'] if num_graded_problems and block_type == 'sequential': questions = ngettext('({number} Question)', '({number} Questions)', num_graded_problems) @@ -55,6 +57,7 @@ def get_blocks(self, block): 'resume_block': block.get('resume_block', False), 'type': block_type, 'has_scheduled_content': block.get('has_scheduled_content'), + 'hash_key': hash_key, }, } for child in children: @@ -128,3 +131,4 @@ class OutlineTabSerializer(DatesBannerSerializerMixin, VerifiedModeSerializerMix resume_course = ResumeCourseSerializer() welcome_message_html = serializers.CharField() user_has_passing_grade = serializers.BooleanField() + mfe_short_url_is_active = serializers.BooleanField() diff --git a/lms/djangoapps/course_home_api/outline/v1/views.py b/lms/djangoapps/course_home_api/outline/v1/views.py index f842275a3752..1f95e492a5d3 100644 --- a/lms/djangoapps/course_home_api/outline/v1/views.py +++ b/lms/djangoapps/course_home_api/outline/v1/views.py @@ -56,7 +56,7 @@ dismiss_current_update_for_user, get_current_update_for_user ) -from openedx.features.course_experience.url_helpers import get_learning_mfe_home_url +from openedx.features.course_experience.url_helpers import get_learning_mfe_home_url, get_usage_key_hash from openedx.features.course_experience.utils import get_course_outline_block_tree, get_start_block from openedx.features.discounts.utils import generate_offer_data from xmodule.course_module import COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE @@ -108,6 +108,7 @@ class OutlineTabView(RetrieveAPIView): the child blocks. resume_block: (bool) Whether the block is the resume block has_scheduled_content: (bool) Whether the block has more content scheduled for the future + hash_key: (str) The blake2b hash of the usage ID of the block. course_goals: selected_goal: days_per_week: (int) The number of days the learner wants to learn per week @@ -152,6 +153,8 @@ class OutlineTabView(RetrieveAPIView): url: (str) The display name of the course block to resume welcome_message_html: (str) Raw HTML for the course updates banner user_has_passing_grade: (bool) Whether the user currently is passing the course + mfe_short_url_is_active: Flag for the learning mfe on whether or not + the url will contain the block id or hash key. **Returns** @@ -237,6 +240,7 @@ def get(self, request, *args, **kwargs): is_enrolled = enrollment and enrollment.is_active is_staff = bool(has_access(request.user, 'staff', course_key)) show_enrolled = is_enrolled or is_staff + mfe_short_url_is_active = settings.ENABLE_SHORT_MFE_URL if show_enrolled: course_blocks = get_course_outline_block_tree(request, course_key_string, request.user) date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1) @@ -318,7 +322,6 @@ def get(self, request, *args, **kwargs): course_key, request.user, datetime.now(tz=timezone.utc) ) available_seq_ids = {str(usage_key) for usage_key in user_course_outline.sequences} - # course_blocks is a reference to the root of the course, so we go # through the chapters (sections) to look for sequences to remove. for chapter_data in course_blocks['children']: @@ -355,6 +358,7 @@ def get(self, request, *args, **kwargs): 'resume_course': resume_course, 'user_has_passing_grade': user_has_passing_grade, 'welcome_message_html': welcome_message_html, + 'mfe_short_url_is_active': mfe_short_url_is_active, } context = self.get_serializer_context() context['course_overview'] = course_overview diff --git a/lms/djangoapps/courseware/tests/test_date_summary.py b/lms/djangoapps/courseware/tests/test_date_summary.py index ec3dba5d0779..0e4214a2259b 100644 --- a/lms/djangoapps/courseware/tests/test_date_summary.py +++ b/lms/djangoapps/courseware/tests/test_date_summary.py @@ -735,7 +735,7 @@ def test_dates_tab_link_render(self, url_name, legacy_active): if legacy_active: html_elements.append('/courses/' + str(course.id) + '/dates') else: - html_elements.append('/course/' + str(course.id) + '/dates') + html_elements.append('/c/' + str(course.id) + '/dates') url = reverse(url_name, args=(course.id,)) def assert_html_elements(assert_function, user): diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 0e1462671c44..e12a0cab52d0 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -90,6 +90,7 @@ get_courseware_url, make_learning_mfe_courseware_url, ExperienceOption, + get_usage_key_hash, ) from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired from common.djangoapps.student.models import CourseEnrollment @@ -161,7 +162,11 @@ def test_jump_to_legacy_vs_mfe(self, activate_mfe, experience_param, expect_mfe) chapter = ItemFactory.create(category='chapter', parent_location=course.location) querystring = f"experience={experience_param}" if experience_param else "" if expect_mfe: - expected_url = f'http://learning-mfe/course/{course.id}/{chapter.location}' + if settings.ENABLE_SHORT_MFE_URL: + expected_url = f'http://learning-mfe/c/{course.id}/{get_usage_key_hash(chapter.location)}' + + else: + expected_url = f'http://learning-mfe/c/{course.id}/{chapter.location}' else: expected_url = f'/courses/{course.id}/courseware/{chapter.url_name}/' @@ -191,7 +196,7 @@ def test_jump_to_invalid_location(self, activate_mfe, store_type): course = CourseFactory.create() location = course.id.make_usage_key(None, 'NoSuchPlace') expected_redirect_url = ( - f'http://learning-mfe/course/{course.id}' + f'http://learning-mfe/c/{course.id}' ) if activate_mfe else ( f'/courses/{course.id}/courseware?' + urlencode({'activate_block_id': str(course.location)}) ) @@ -224,8 +229,12 @@ def test_jump_to_mfe_from_sequence(self): chapter = ItemFactory.create(category='chapter', parent_location=course.location) sequence = ItemFactory.create(category='sequential', parent_location=chapter.location) expected_redirect_url = ( - f'http://learning-mfe/course/{course.id}/{sequence.location}' + f'http://learning-mfe/c/{course.id}/{sequence.location}' ) + if settings.ENABLE_SHORT_MFE_URL: + expected_redirect_url = ( + f'http://learning-mfe/c/{course.id}/{get_usage_key_hash(sequence.location)}' + ) jumpto_url = f'/courses/{course.id}/jump_to/{sequence.location}' response = self.client.get(jumpto_url) assert response.status_code == 302 @@ -270,16 +279,26 @@ def test_jump_to_mfe_from_module(self): module2 = ItemFactory.create(category='html', parent_location=vertical2.location) expected_redirect_url = ( - f'http://learning-mfe/course/{course.id}/{sequence.location}/{vertical1.location}' + f'http://learning-mfe/c/{course.id}/{sequence.location}/{vertical1.location}' ) + if settings.ENABLE_SHORT_MFE_URL: + expected_redirect_url = ( + f'http://learning-mfe/c/{course.id}/{get_usage_key_hash(sequence.location)}' + f'/{get_usage_key_hash(vertical1.location)}' + ) jumpto_url = f'/courses/{course.id}/jump_to/{module1.location}' response = self.client.get(jumpto_url) assert response.status_code == 302 assert response.url == expected_redirect_url expected_redirect_url = ( - f'http://learning-mfe/course/{course.id}/{sequence.location}/{vertical2.location}' + f'http://learning-mfe/c/{course.id}/{sequence.location}/{vertical2.location}' ) + if settings.ENABLE_SHORT_MFE_URL: + expected_redirect_url = ( + f'http://learning-mfe/c/{course.id}/{get_usage_key_hash(sequence.location)}' + f'/{get_usage_key_hash(vertical2.location)}' + ) jumpto_url = f'/courses/{course.id}/jump_to/{module2.location}' response = self.client.get(jumpto_url) assert response.status_code == 302 @@ -489,14 +508,19 @@ def _get_urls(self): # lint-amnesty, pylint: disable=missing-function-docstring 'section': str(self.section2.location.block_id), } ) - mfe_url = '{}/course/{}/{}'.format( + mfe_url = '{}/c/{}/{}'.format( settings.LEARNING_MICROFRONTEND_URL, self.course_key, self.section2.location ) + short_mfe_url = '{}/c/{}/{}'.format( + settings.LEARNING_MICROFRONTEND_URL, + self.course_key, + get_usage_key_hash(self.section2.location) + ) preview_url = "http://" + settings.FEATURES.get('PREVIEW_LMS_BASE') + lms_url - return lms_url, mfe_url, preview_url + return lms_url, mfe_url, short_mfe_url, preview_url @ddt.ddt @@ -3505,21 +3529,40 @@ def test_url_generation(self): course_key = CourseKey.from_string("course-v1:OpenEdX+MFE+2020") section_key = UsageKey.from_string("block-v1:OpenEdX+MFE+2020+type@sequential+block@Introduction") unit_id = "block-v1:OpenEdX+MFE+2020+type@vertical+block@Getting_To_Know_You" - assert make_learning_mfe_courseware_url(course_key) == ( - 'https://learningmfe.openedx.org' - '/course/course-v1:OpenEdX+MFE+2020' - ) - assert make_learning_mfe_courseware_url(course_key, section_key, '') == ( - 'https://learningmfe.openedx.org' - '/course/course-v1:OpenEdX+MFE+2020' - '/block-v1:OpenEdX+MFE+2020+type@sequential+block@Introduction' - ) - assert make_learning_mfe_courseware_url(course_key, section_key, unit_id) == ( - 'https://learningmfe.openedx.org' - '/course/course-v1:OpenEdX+MFE+2020' - '/block-v1:OpenEdX+MFE+2020+type@sequential+block@Introduction' - '/block-v1:OpenEdX+MFE+2020+type@vertical+block@Getting_To_Know_You' - ) + if settings.ENABLE_SHORT_MFE_URL: + short_section_key = get_usage_key_hash(section_key) + short_unit_id = get_usage_key_hash(unit_id) + assert make_learning_mfe_courseware_url(course_key) == ( + 'https://learningmfe.openedx.org' + '/c/course-v1:OpenEdX+MFE+2020' + ) + assert make_learning_mfe_courseware_url(course_key, short_section_key, '') == ( + 'https://learningmfe.openedx.org' + '/c/course-v1:OpenEdX+MFE+2020' + '/YmJiOTNjMGJiYTQ2' + ) + assert make_learning_mfe_courseware_url(course_key, short_section_key, short_unit_id) == ( + 'https://learningmfe.openedx.org' + '/c/course-v1:OpenEdX+MFE+2020' + '/YmJiOTNjMGJiYTQ2' + '/MzI4ODU4MjY5YzVi' + ) + else: + assert make_learning_mfe_courseware_url(course_key) == ( + 'https://learningmfe.openedx.org' + '/c/course-v1:OpenEdX+MFE+2020' + ) + assert make_learning_mfe_courseware_url(course_key, section_key, '') == ( + 'https://learningmfe.openedx.org' + '/c/course-v1:OpenEdX+MFE+2020' + '/block-v1:OpenEdX+MFE+2020+type@sequential+block@Introduction' + ) + assert make_learning_mfe_courseware_url(course_key, section_key, unit_id) == ( + 'https://learningmfe.openedx.org' + '/c/course-v1:OpenEdX+MFE+2020' + '/block-v1:OpenEdX+MFE+2020+type@sequential+block@Introduction' + '/block-v1:OpenEdX+MFE+2020+type@vertical+block@Getting_To_Know_You' + ) @ddt.ddt @@ -3528,12 +3571,15 @@ class MFERedirectTests(BaseViewsTestCase): # lint-amnesty, pylint: disable=miss def test_learner_redirect(self): # learners will be redirected when the waffle flag is set - lms_url, mfe_url, __ = self._get_urls() + lms_url, mfe_url, short_mfe_url, __ = self._get_urls() - assert self.client.get(lms_url).url == mfe_url + if settings.ENABLE_SHORT_MFE_URL: + assert self.client.get(lms_url).url == short_mfe_url + else: + assert self.client.get(lms_url).url == mfe_url def test_staff_no_redirect(self): - lms_url, __, __ = self._get_urls() + lms_url, __, __, __ = self._get_urls() # course staff will redirect in an MFE-enabled course - and not redirect otherwise. course_staff = UserFactory.create(is_staff=False) @@ -3556,7 +3602,7 @@ def test_exam_no_redirect(self): self.section2.is_time_limited = True self.store.update_item(self.section2, self.user.id) - lms_url, __, __ = self._get_urls() + lms_url, __, __, __ = self._get_urls() assert self.client.get(lms_url).status_code == 200 @@ -3574,7 +3620,7 @@ class PreviewRedirectTests(BaseViewsTestCase): MODULESTORE = TEST_DATA_SPLIT_MODULESTORE def test_staff_no_redirect(self): - __, __, preview_url = self._get_urls() + __, __, __, preview_url = self._get_urls() with patch.object(access_utils, 'get_current_request_hostname', return_value=settings.FEATURES.get('PREVIEW_LMS_BASE', None)): @@ -3598,7 +3644,7 @@ def test_exam_no_redirect(self): self.section2.is_time_limited = True self.store.update_item(self.section2, self.user.id) - __, __, preview_url = self._get_urls() + __, __, __, preview_url = self._get_urls() assert self.client.get(preview_url).status_code == 200 diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index bdd14da23d22..42741c820a06 100644 --- a/lms/djangoapps/courseware/views/index.py +++ b/lms/djangoapps/courseware/views/index.py @@ -205,6 +205,7 @@ def microfrontend_url(self): unit_key = None except InvalidKeyError: unit_key = None + url = make_learning_mfe_courseware_url( self.course_key, self.section.location if self.section else None, diff --git a/lms/envs/common.py b/lms/envs/common.py index d5b3ac2b1afc..5cce765d38c5 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -4788,6 +4788,17 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring # Token for " Disable Different Origin Subframe Dialog Suppression" for http://localhost:18000 CHROME_DISABLE_SUBFRAME_DIALOG_SUPPRESSION_TOKEN = 'ArNBN7d1AkvMhJTGWXlJ8td/AN4lOokzOnqKRNkTnLqaqx0HpfYvmx8JePPs/emKh6O5fckx14LeZIGJ1AQYjgAAAABzeyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjE4MDAwIiwiZmVhdHVyZSI6IkRpc2FibGVEaWZmZXJlbnRPcmlnaW5TdWJmcmFtZURpYWxvZ1N1cHByZXNzaW9uIiwiZXhwaXJ5IjoxNjM5NTI2Mzk5fQ==' # pylint: disable=line-too-long +############# Shorten MFE URL ########################## +# .. toggle_name: enable_short_mfe_url +# .. toggle_implementation: DjangoSetting +# .. toggle_default: False +# .. toggle_description: Flag would be used to hash block_ids and shorten the MFE url +# .. toggle_use_cases: temporary +# .. toggle_tickets: https://github.com/edx/edx-platform/pull/28174/ +# .. toggle_creation_date: 2021-08-03 +# .. toggle_target_removal_date: 2022-01-01 +ENABLE_SHORT_MFE_URL = False + ################# Documentation links for course apps ################# # pylint: disable=line-too-long diff --git a/openedx/core/djangoapps/content/learning_sequences/data.py b/openedx/core/djangoapps/content/learning_sequences/data.py index d7efe0e0a166..2cf414729c80 100644 --- a/openedx/core/djangoapps/content/learning_sequences/data.py +++ b/openedx/core/djangoapps/content/learning_sequences/data.py @@ -125,6 +125,15 @@ class CourseLearningSequenceData: learning sequences in Courses vs. Pathways vs. Libraries. Such an object would likely not have `visibility` as that holds course-specific concepts. """ + + mapping = {} + + def short_id_mapping(self, hash_key, *args, **kwargs): + usage_key_id = kwargs.get('usage_key', None) + if usage_key_id is None: + return self.mapping[hash_key] + self.mapping[hash_key] = usage_key_id + usage_key = attr.ib(type=UsageKey) title = attr.ib(type=str) visibility = attr.ib(type=VisibilityData, default=VisibilityData()) diff --git a/openedx/core/djangoapps/courseware_api/serializers.py b/openedx/core/djangoapps/courseware_api/serializers.py index 2395cc1761d7..f67074f46f37 100644 --- a/openedx/core/djangoapps/courseware_api/serializers.py +++ b/openedx/core/djangoapps/courseware_api/serializers.py @@ -119,6 +119,7 @@ class CourseInfoSerializer(serializers.Serializer): # pylint: disable=abstract- is_mfe_special_exams_enabled = serializers.BooleanField() is_mfe_proctored_exams_enabled = serializers.BooleanField() user_needs_integrity_signature = serializers.BooleanField() + mfe_short_url_is_active = serializers.BooleanField() def __init__(self, *args, **kwargs): """ diff --git a/openedx/core/djangoapps/courseware_api/views.py b/openedx/core/djangoapps/courseware_api/views.py index fad88a508fa1..35cecc5dc3fb 100644 --- a/openedx/core/djangoapps/courseware_api/views.py +++ b/openedx/core/djangoapps/courseware_api/views.py @@ -2,6 +2,9 @@ Course API Views """ +from base64 import ( + urlsafe_b64decode, binascii +) from completion.exceptions import UnavailableCompletionData from completion.utilities import get_key_to_last_completed_block from django.conf import settings @@ -44,12 +47,14 @@ from lms.djangoapps.verify_student.services import IDVerificationService from openedx.core.djangoapps.agreements.api import get_integrity_signature from openedx.core.djangoapps.agreements.toggles import is_integrity_signature_enabled +from openedx.core.djangoapps.content.learning_sequences.data import CourseLearningSequenceData from openedx.core.djangoapps.courseware_api.utils import get_celebrations_dict from openedx.core.djangoapps.programs.utils import ProgramProgressMeter from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin from openedx.core.lib.courses import get_course_by_id from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG +from openedx.features.course_experience.url_helpers import get_usage_key_hash from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.course_duration_limits.access import get_access_expiration_data from openedx.features.discounts.utils import generate_offer_data @@ -59,8 +64,8 @@ LinkedInAddToProfileConfiguration ) from xmodule.modulestore.django import modulestore -from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem from xmodule.modulestore.search import path_to_location +from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem from xmodule.x_module import PUBLIC_VIEW, STUDENT_VIEW from .serializers import CourseInfoSerializer @@ -347,6 +352,13 @@ def user_timezone(self): user_timezone_locale = user_timezone_locale_prefs(self.request) return user_timezone_locale['user_timezone'] + @property + def mfe_short_url_is_active(self): + """ + Returns a boolean on if the course exit page is active + """ + return settings.ENABLE_SHORT_MFE_URL + class CoursewareInformation(RetrieveAPIView): """ @@ -423,7 +435,9 @@ class CoursewareInformation(RetrieveAPIView): * verify_identity_url: URL for a learner to verify their identity. Only returned for learners enrolled in a verified mode. Will update to reverify URL if necessary. * linkedin_add_to_profile_url: URL to add the effective user's certificate to a LinkedIn Profile. - * user_needs_integrity_signature: Whether the user needs to sign the integrity agreement for the course + * user_needs_integrity_signature: Whether the user needs to sign the integrity agreement for the course. + * mfe_short_url_is_active: Flag for the learning mfe on whether or not + the url will contain the block id or hash key. **Parameters:** @@ -522,8 +536,18 @@ def get(self, request, usage_key_string, *args, **kwargs): # lint-amnesty, pyli """ Return response to a GET request. """ + if settings.ENABLE_SHORT_MFE_URL: + try: + decoded_hash_string = urlsafe_b64decode(usage_key_string) + usage_key_hash = decoded_hash_string.decode('utf-8') + usage_key_string = str(CourseLearningSequenceData.short_id_mapping(CourseLearningSequenceData, + hash_key=usage_key_hash)) + except binascii.Error: + get_usage_key_hash(usage_key_string) + try: usage_key = UsageKey.from_string(usage_key_string) + except InvalidKeyError: raise NotFound(f"Invalid usage key: '{usage_key_string}'.") # lint-amnesty, pylint: disable=raise-missing-from @@ -533,7 +557,6 @@ def get(self, request, usage_key_string, *args, **kwargs): # lint-amnesty, pyli staff_access=has_access(request.user, 'staff', usage_key.course_key), reset_masquerade_data=True, ) - sequence, _ = get_module_by_usage_id( self.request, str(usage_key.course_key), @@ -544,8 +567,13 @@ def get(self, request, usage_key_string, *args, **kwargs): # lint-amnesty, pyli view = STUDENT_VIEW if request.user.is_anonymous: view = PUBLIC_VIEW - - return Response(sequence.get_metadata(view=view)) + metadata = sequence.get_metadata(view=view) + metadata['decoded_id'] = metadata['item_id'] + metadata['hash_key'] = get_usage_key_hash(metadata['item_id']) + for item in metadata['items']: + item['decoded_id'] = item['id'] + item['hash_key'] = get_usage_key_hash(item['id']) + return Response(metadata) class Resume(DeveloperErrorViewMixin, APIView): @@ -596,9 +624,14 @@ def get(self, request, course_key_string, *args, **kwargs): # lint-amnesty, pyl try: block_key = get_key_to_last_completed_block(request.user, course_id) path = path_to_location(modulestore(), block_key, request, full_path=True) - resp['section_id'] = str(path[2]) - resp['unit_id'] = str(path[3]) - resp['block_id'] = str(block_key) + if settings.ENABLE_SHORT_MFE_URL: + resp['section_id'] = get_usage_key_hash(path[2]) + resp['unit_id'] = get_usage_key_hash(path[3]) + resp['block_id'] = str(block_key) + else: + resp['section_id'] = str(path[2]) + resp['unit_id'] = str(path[3]) + resp['block_id'] = str(block_key) except (ItemNotFoundError, NoPathToItem, UnavailableCompletionData): pass # leaving all the IDs as None indicates a redirect to the first unit in the course, as a backup diff --git a/openedx/features/course_experience/tests/test_url_helpers.py b/openedx/features/course_experience/tests/test_url_helpers.py index 4728858cf0a2..b1c86ba78441 100644 --- a/openedx/features/course_experience/tests/test_url_helpers.py +++ b/openedx/features/course_experience/tests/test_url_helpers.py @@ -173,14 +173,14 @@ def create_test_courses(cls): ModuleStoreEnum.Type.split, 'mfe', 'course_run', - 'http://learning-mfe/course/course-v1:TestX+UrlHelpers+split' + 'http://learning-mfe/c/course-v1:TestX+UrlHelpers+split' ), ( ModuleStoreEnum.Type.split, 'mfe', 'section', ( - 'http://learning-mfe/course/course-v1:TestX+UrlHelpers+split' + + 'http://learning-mfe/c/course-v1:TestX+UrlHelpers+split' + '/block-v1:TestX+UrlHelpers+split+type@chapter+block@Generated_Section' ), ), @@ -189,7 +189,7 @@ def create_test_courses(cls): 'mfe', 'subsection', ( - 'http://learning-mfe/course/course-v1:TestX+UrlHelpers+split' + + 'http://learning-mfe/c/course-v1:TestX+UrlHelpers+split' + '/block-v1:TestX+UrlHelpers+split+type@sequential+block@Generated_Subsection' ), ), @@ -198,7 +198,7 @@ def create_test_courses(cls): 'mfe', 'unit', ( - 'http://learning-mfe/course/course-v1:TestX+UrlHelpers+split' + + 'http://learning-mfe/c/course-v1:TestX+UrlHelpers+split' + '/block-v1:TestX+UrlHelpers+split+type@sequential+block@Generated_Subsection' + '/block-v1:TestX+UrlHelpers+split+type@vertical+block@Generated_Unit' ), @@ -208,7 +208,7 @@ def create_test_courses(cls): 'mfe', 'component', ( - 'http://learning-mfe/course/course-v1:TestX+UrlHelpers+split' + + 'http://learning-mfe/c/course-v1:TestX+UrlHelpers+split' + '/block-v1:TestX+UrlHelpers+split+type@sequential+block@Generated_Subsection' + '/block-v1:TestX+UrlHelpers+split+type@vertical+block@Generated_Unit' ), diff --git a/openedx/features/course_experience/url_helpers.py b/openedx/features/course_experience/url_helpers.py index ecc5515f89db..7bd84cda9d50 100644 --- a/openedx/features/course_experience/url_helpers.py +++ b/openedx/features/course_experience/url_helpers.py @@ -6,6 +6,8 @@ """ from enum import Enum from typing import Optional +from base64 import urlsafe_b64encode +from hashlib import blake2b import six # lint-amnesty, pylint: disable=unused-import from django.conf import settings @@ -18,6 +20,8 @@ from lms.djangoapps.courseware.toggles import courseware_mfe_is_active from xmodule.modulestore.django import modulestore from xmodule.modulestore.search import navigation_index, path_to_location +from openedx.core.djangoapps.content.learning_sequences.data import CourseLearningSequenceData + User = get_user_model() @@ -65,7 +69,7 @@ def get_courseware_url( get_url_fn = _get_new_courseware_url else: get_url_fn = _get_legacy_courseware_url - return get_url_fn(usage_key=usage_key, request=request) + return get_url_fn(usage_key=usage_key, request=request,) def _get_legacy_courseware_url( @@ -144,6 +148,26 @@ def _get_new_courseware_url( ) +def get_usage_key_hash(usage_key): + ''' + Get the blake2b hash key for the given usage_key and encode the value. The + hash key will be added to the usage key's mapping dictionary for decoding + in LMS. + + Args: + usage_key: :class:`UsageKey` the id of the location to which to generate the path + + Returns: + The string of the encoded hash key. + ''' + + short_key = blake2b(bytes(str(usage_key), 'utf-8'), digest_size=6) + encoded_hash = urlsafe_b64encode(bytes(short_key.hexdigest(), 'utf-8')) + CourseLearningSequenceData.short_id_mapping(CourseLearningSequenceData, hash_key=short_key.hexdigest(), + usage_key=usage_key) + return str(encoded_hash, 'utf-8') + + def make_learning_mfe_courseware_url( course_key: CourseKey, sequence_key: Optional[UsageKey] = None, @@ -172,17 +196,25 @@ def make_learning_mfe_courseware_url( We're building a URL like this: - http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course/block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5/block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76 + http://localhost:2000/c/course-v1:edX+DemoX+Demo_Course/block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5/block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76 + + If `settings.ENABLE_SHORT_MFE_URL` is set to True, the URL will be built + like this: + + http://localhost:2000/c/course-v1:edX+DemoX+Demo_Course/ `course_key`, `sequence_key`, and `unit_key` can be either OpaqueKeys or strings. They're only ever used to concatenate a URL string. """ - mfe_link = f'{settings.LEARNING_MICROFRONTEND_URL}/course/{course_key}' - + mfe_link = f'{settings.LEARNING_MICROFRONTEND_URL}/c/{course_key}' if sequence_key: + if settings.ENABLE_SHORT_MFE_URL: + sequence_key = get_usage_key_hash(sequence_key) mfe_link += f'/{sequence_key}' if unit_key: + if settings.ENABLE_SHORT_MFE_URL: + unit_key = get_usage_key_hash(unit_key) mfe_link += f'/{unit_key}' return mfe_link @@ -196,12 +228,12 @@ def get_learning_mfe_home_url( We're building a URL like this: - http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course/dates + http://localhost:2000/c/course-v1:edX+DemoX+Demo_Course/dates `course_key` can be either an OpaqueKey or a string. `view_name` is an optional string. """ - mfe_link = f'{settings.LEARNING_MICROFRONTEND_URL}/course/{course_key}' + mfe_link = f'{settings.LEARNING_MICROFRONTEND_URL}/c/{course_key}' if view_name: mfe_link += f'/{view_name}' diff --git a/openedx/features/course_experience/utils.py b/openedx/features/course_experience/utils.py index b3dbbfae20c7..d51a459b4b2d 100644 --- a/openedx/features/course_experience/utils.py +++ b/openedx/features/course_experience/utils.py @@ -10,6 +10,7 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.lib.cache_utils import request_cached from openedx.features.course_experience import RELATIVE_DATES_FLAG +from openedx.features.course_experience.url_helpers import get_usage_key_hash from common.djangoapps.student.models import CourseEnrollment from xmodule.modulestore.django import modulestore @@ -36,6 +37,7 @@ def populate_children(block, all_blocks): of those children. """ children = block.get('children', []) + block['hash_key'] = get_usage_key_hash(block['id']) for i in range(len(children)): child_id = block['children'][i]