diff --git a/src/course-home/dates-tab/DatesTab.test.jsx b/src/course-home/dates-tab/DatesTab.test.jsx index d0937772..c2ce6d32 100644 --- a/src/course-home/dates-tab/DatesTab.test.jsx +++ b/src/course-home/dates-tab/DatesTab.test.jsx @@ -81,7 +81,7 @@ describe('DatesTab', () => { beforeEach(() => { axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata); axiosMock.onGet(datesUrl).reply(200, datesTabData); - history.push(`/course/${courseId}/dates`); // so tab can pull course id from url + history.push(`/c/${courseId}/dates`); // so tab can pull course id from url render(component); }); @@ -147,7 +147,7 @@ describe('DatesTab', () => { describe('Suggested schedule messaging', () => { beforeEach(() => { setMetadata({ is_self_paced: true, is_enrolled: true }); - history.push(`/course/${courseId}/dates`); + history.push(`/c/${courseId}/dates`); }); it('renders SuggestedScheduleHeader', async () => { @@ -316,7 +316,7 @@ describe('DatesTab', () => { beforeEach(() => { axiosMock.onGet(datesUrl).reply(200, datesTabData); - history.push(`/course/${courseId}/dates`); // so tab can pull course id from url + history.push(`/c/${courseId}/dates`); // so tab can pull course id from url }); it('redirects to course survey for a survey_required error code', async () => { diff --git a/src/course-home/outline-tab/OutlineTab.test.jsx b/src/course-home/outline-tab/OutlineTab.test.jsx index 040f3ce0..acf0f820 100644 --- a/src/course-home/outline-tab/OutlineTab.test.jsx +++ b/src/course-home/outline-tab/OutlineTab.test.jsx @@ -156,7 +156,7 @@ describe('Outline Tab', () => { await fetchAndRender(); const sequenceLink = screen.getByText('Title of Sequence'); - expect(sequenceLink.getAttribute('href')).toContain(`/course/${courseId}`); + expect(sequenceLink.getAttribute('href')).toContain(`/c/${courseId}`); }); }); diff --git a/src/courseware/CoursewareContainer.test.jsx b/src/courseware/CoursewareContainer.test.jsx index e598969d..0cb111e6 100644 --- a/src/courseware/CoursewareContainer.test.jsx +++ b/src/courseware/CoursewareContainer.test.jsx @@ -85,9 +85,9 @@ describe('CoursewareContainer', () => { @@ -241,7 +241,7 @@ describe('CoursewareContainer', () => { } function assertLocation(container, sequenceId, unitId) { - const expectedUrl = `http://localhost/course/${courseId}/${sequenceId}/${unitId}`; + const expectedUrl = `http://localhost/c/${courseId}/${sequenceId}/${unitId}`; expect(global.location.href).toEqual(expectedUrl); expect(container.querySelector('.fake-unit')).toHaveTextContent(unitId); } @@ -293,14 +293,14 @@ describe('CoursewareContainer', () => { it('should ignore the section ID and instead redirect to the course root', async () => { setUrl(sectionTree[1].id); await loadContainer(); - expect(global.location.href).toEqual(`http://localhost/course/${courseId}`); + expect(global.location.href).toEqual(`http://localhost/c/${courseId}`); }); it('should ignore the section and unit IDs and instead to the course root', async () => { // Specific unit ID used here shouldn't matter; is ignored due to empty section. setUrl(sectionTree[1].id, unitTree[0][0][0]); await loadContainer(); - expect(global.location.href).toEqual(`http://localhost/course/${courseId}`); + expect(global.location.href).toEqual(`http://localhost/c/${courseId}`); }); }); }); @@ -314,13 +314,13 @@ describe('CoursewareContainer', () => { it('should insert the sequence ID into the URL', async () => { const unit = unitTree[1][0][1]; - history.push(`/course/${courseId}/${unit.id}`); + history.push(`/c/${courseId}/${unit.id}`); const container = await loadContainer(); assertLoadedHeader(container); assertSequenceNavigation(container, 2); const expectedSequenceId = sequenceTree[1][0].id; - const expectedUrl = `http://localhost/course/${courseId}/${expectedSequenceId}/${unit.id}`; + const expectedUrl = `http://localhost/c/${courseId}/${expectedSequenceId}/${unit.id}`; expect(global.location.href).toEqual(expectedUrl); expect(container.querySelector('.fake-unit')).toHaveTextContent(unit.id); }); @@ -331,7 +331,7 @@ describe('CoursewareContainer', () => { const unitBlocks = defaultUnitBlocks; it('should pick the first unit if position was not defined (activeUnitIndex becomes 0)', async () => { - history.push(`/course/${courseId}/${sequenceBlock.id}`); + history.push(`/c/${courseId}/${sequenceBlock.id}`); const container = await loadContainer(); assertLoadedHeader(container); @@ -350,7 +350,7 @@ describe('CoursewareContainer', () => { ); setUpMockRequests({ sequenceMetadatas: [sequenceMetadata] }); - history.push(`/course/${courseId}/${sequenceBlock.id}`); + history.push(`/c/${courseId}/${sequenceBlock.id}`); const container = await loadContainer(); assertLoadedHeader(container); @@ -367,7 +367,7 @@ describe('CoursewareContainer', () => { const unitBlocks = defaultUnitBlocks; it('should load the specified unit', async () => { - history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[2].id}`); + history.push(`/c/${courseId}/${sequenceBlock.id}/${unitBlocks[2].id}`); const container = await loadContainer(); assertLoadedHeader(container); @@ -391,7 +391,7 @@ describe('CoursewareContainer', () => { expect(sequenceNextButton).toHaveTextContent('Next'); fireEvent.click(sequenceNavButtons[4]); - expect(global.location.href).toEqual(`http://localhost/course/${courseId}/${sequenceBlock.id}/${unitBlocks[1].id}`); + expect(global.location.href).toEqual(`http://localhost/c/${courseId}/${sequenceBlock.id}/${unitBlocks[1].id}`); }); }); @@ -419,7 +419,7 @@ describe('CoursewareContainer', () => { ); setUpMockRequests({ sequenceMetadatas: [sequenceMetadata] }); - history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[2].id}`); + history.push(`/c/${courseId}/${sequenceBlock.id}/${unitBlocks[2].id}`); await loadContainer(); expect(global.location.assign).toHaveBeenCalledWith(sequenceBlock.legacy_web_url); @@ -440,7 +440,7 @@ describe('CoursewareContainer', () => { const { courseBlocks, sequenceBlocks, unitBlocks } = buildSimpleCourseBlocks(courseId, courseMetadata.name); setUpMockRequests({ courseBlocks, courseMetadata }); - history.push(`/course/${courseId}/${sequenceBlocks[0].id}/${unitBlocks[0].id}`); + history.push(`/c/${courseId}/${sequenceBlocks[0].id}/${unitBlocks[0].id}`); return { courseMetadata, unitBlocks }; } diff --git a/src/courseware/course/course-exit/CourseExit.test.jsx b/src/courseware/course/course-exit/CourseExit.test.jsx index 0a1e2c2e..f5654979 100644 --- a/src/courseware/course/course-exit/CourseExit.test.jsx +++ b/src/courseware/course/course-exit/CourseExit.test.jsx @@ -94,7 +94,7 @@ describe('Course Exit Pages', () => { }, }); await fetchAndRender(); - expect(global.location.href).toEqual(`http://localhost/course/${defaultMetadata.id}`); + expect(global.location.href).toEqual(`http://localhost/c/${defaultMetadata.id}`); }); }); diff --git a/src/courseware/course/sequence/content-lock/ContentLock.jsx b/src/courseware/course/sequence/content-lock/ContentLock.jsx index 2ec0f00b..29a6e551 100644 --- a/src/courseware/course/sequence/content-lock/ContentLock.jsx +++ b/src/courseware/course/sequence/content-lock/ContentLock.jsx @@ -12,7 +12,7 @@ function ContentLock({ intl, courseId, prereqSectionName, prereqId, sequenceTitle, }) { const handleClick = useCallback(() => { - history.push(`/course/${courseId}/${prereqId}`); + history.push(`/c/${courseId}/${prereqId}`); }); return ( diff --git a/src/courseware/course/sequence/content-lock/ContentLock.test.jsx b/src/courseware/course/sequence/content-lock/ContentLock.test.jsx index 500b5078..39da5c83 100644 --- a/src/courseware/course/sequence/content-lock/ContentLock.test.jsx +++ b/src/courseware/course/sequence/content-lock/ContentLock.test.jsx @@ -38,6 +38,6 @@ describe('Content Lock', () => { render(); fireEvent.click(screen.getByRole('button')); - expect(history.push).toHaveBeenCalledWith(`/course/${mockData.courseId}/${mockData.prereqId}`); + expect(history.push).toHaveBeenCalledWith(`/c/${mockData.courseId}/${mockData.prereqId}`); }); }); diff --git a/src/courseware/course/sequence/honor-code/HonorCode.jsx b/src/courseware/course/sequence/honor-code/HonorCode.jsx index 1d9f3999..2c0df285 100644 --- a/src/courseware/course/sequence/honor-code/HonorCode.jsx +++ b/src/courseware/course/sequence/honor-code/HonorCode.jsx @@ -13,7 +13,7 @@ function HonorCode({ intl, courseId }) { const siteName = getConfig().SITE_NAME; const honorCodeUrl = `${process.env.TERMS_OF_SERVICE_URL}#honor-code`; - const handleCancel = () => history.push(`/course/${courseId}/home`); + const handleCancel = () => history.push(`/c/${courseId}/home`); const handleAgree = () => { dispatch(saveIntegritySignature(courseId)); diff --git a/src/courseware/course/sequence/honor-code/HonorCode.test.jsx b/src/courseware/course/sequence/honor-code/HonorCode.test.jsx index 2708dcbe..3a5b902a 100644 --- a/src/courseware/course/sequence/honor-code/HonorCode.test.jsx +++ b/src/courseware/course/sequence/honor-code/HonorCode.test.jsx @@ -28,6 +28,6 @@ describe('Honor Code', () => { const cancelButton = screen.getByText('Cancel'); fireEvent.click(cancelButton); - expect(history.push).toHaveBeenCalledWith(`/course/${mockData.courseId}/home`); + expect(history.push).toHaveBeenCalledWith(`/c/${mockData.courseId}/home`); }); }); diff --git a/src/courseware/data/__factories__/sequenceMetadata.factory.js b/src/courseware/data/__factories__/sequenceMetadata.factory.js index 3ec46b8e..788611fa 100644 --- a/src/courseware/data/__factories__/sequenceMetadata.factory.js +++ b/src/courseware/data/__factories__/sequenceMetadata.factory.js @@ -27,7 +27,7 @@ Factory.define('sequenceMetadata') ), ) .attr('element_id', ['sequenceBlock'], sequenceBlock => sequenceBlock.block_id) - .attr('item_id', ['sequenceBlock'], sequenceBlock => sequenceBlock.id) + .attr('item_id', ['sequenceBlock'], sequenceBlock => (sequenceBlock.hash_key || sequenceBlock.id)) .attr('display_name', ['sequenceBlock'], sequenceBlock => sequenceBlock.display_name) .attr('gated_content', ['sequenceBlock'], sequenceBlock => ({ gated: false, @@ -36,6 +36,9 @@ Factory.define('sequenceMetadata') prereq_section_name: `${sequenceBlock.display_name}-prereq`, gated_section_name: sequenceBlock.display_name, })) + + .attr('decoded_id', ['sequenceBlock'], sequenceBlock => sequenceBlock.decoded_id) + .attr('hash_key', ['sequenceBlock'], sequenceBlock => sequenceBlock.hash_key) .attr('items', ['unitBlocks', 'sequenceBlock'], (unitBlocks, sequenceBlock) => unitBlocks.map( unitBlock => ({ href: '', @@ -44,10 +47,12 @@ Factory.define('sequenceMetadata') bookmarked: unitBlock.bookmarked || false, path: `Chapter Display Name > ${sequenceBlock.display_name} > ${unitBlock.display_name}`, type: unitBlock.type, + hash_key: unitBlock.hash_key, complete: unitBlock.complete || null, content: '', page_title: unitBlock.display_name, contains_content_type_gated_content: unitBlock.contains_content_type_gated_content, + decoded_id: unitBlock.decoded_id, }), )) .attrs({ diff --git a/src/setupTest.js b/src/setupTest.js index 4fb05c6e..19e4d0ae 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -158,7 +158,7 @@ export async function initializeTestStore(options = {}, overrideStore = true) { axiosMock.onGet(courseBlocksUrlRegExp).reply(200, courseBlocks); axiosMock.onGet(learningSequencesUrlRegExp).reply(403, {}); sequenceMetadata.forEach(metadata => { - const sequenceMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/sequence/${metadata.item_id}`; + const sequenceMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/sequence/${metadata.decoded_id}`; axiosMock.onGet(sequenceMetadataUrl).reply(200, metadata); const proctoredExamApiUrl = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${courseMetadata.id}/content_id/${sequenceMetadata.item_id}?is_learning_mfe=true`; axiosMock.onGet(proctoredExamApiUrl).reply(200, { exam: {}, active_attempt: {} }); diff --git a/src/shared/data/__factories__/block.factory.js b/src/shared/data/__factories__/block.factory.js index f037c022..b37272ea 100644 --- a/src/shared/data/__factories__/block.factory.js +++ b/src/shared/data/__factories__/block.factory.js @@ -22,10 +22,18 @@ Factory.define('block') return blockId; }) + .attr( + 'hash_key', ['hash_key'], + () => (Math.random().toString(36).substring(2, 15)), + ) .attr( 'id', - ['id', 'block_id', 'type', 'courseId'], - (id, blockId, type, courseId) => { + ['id', 'block_id', 'type', 'courseId', 'hash_key'], + (id, blockId, type, courseId, hashKey) => { + if (hashKey) { + return hashKey; + } + if (id) { return id; } @@ -35,25 +43,33 @@ Factory.define('block') return `block-v1:${courseInfo}+type@${type}+block@${blockId}`; }, ) + .attr( + 'decoded_id', ['block_id', 'type', 'courseId'], + (blockId, type, courseId) => { + const courseInfo = courseId.split(':')[1]; + + return `block-v1:${courseInfo}+type@${type}+block@${blockId}`; + }, + ) .attr( 'student_view_url', - ['student_view_url', 'host', 'id'], - (url, host, id) => { + ['student_view_url', 'host', 'decoded_id'], + (url, host, decodedId) => { if (url) { return url; } - return `${host}/xblock/${id}`; + return `${host}/xblock/${decodedId}`; }, ) .attr( 'legacy_web_url', - ['legacy_web_url', 'host', 'courseId', 'id'], - (url, host, courseId, id) => { + ['legacy_web_url', 'host', 'courseId', 'decoded_id'], + (url, host, courseId, decodedId) => { if (url) { return url; } - return `${host}/courses/${courseId}/jump_to/${id}?experience=legacy`; + return `${host}/courses/${courseId}/jump_to/${decodedId}?experience=legacy`; }, ); diff --git a/src/tab-page/TabContainer.test.jsx b/src/tab-page/TabContainer.test.jsx index 55c81fa9..cd43ec6c 100644 --- a/src/tab-page/TabContainer.test.jsx +++ b/src/tab-page/TabContainer.test.jsx @@ -30,9 +30,9 @@ describe('Tab Container', () => { }); it('renders correctly', () => { - history.push(`/course/${courseId}`); + history.push(`/c/${courseId}`); render( - + , ); @@ -48,11 +48,11 @@ describe('Tab Container', () => { it('Should handle passing in a targetUserId', () => { const targetUserId = '1'; - history.push(`/course/${courseId}/progress/${targetUserId}/`); + history.push(`/c/${courseId}/progress/${targetUserId}/`); render( ( mockFetch(match.params.courseId, match.params.targetUserId)}