Compare commits
65 Commits
dependabot
...
KristinAok
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8882026a01 | ||
|
|
c6578d4e2e | ||
|
|
fe4680646e | ||
|
|
c09ba48615 | ||
|
|
c46da1dc34 | ||
|
|
9ca5c61088 | ||
|
|
a17e2a1a15 | ||
|
|
ea02b2f70f | ||
|
|
5fa33e4015 | ||
|
|
569b628961 | ||
|
|
43eb58974a | ||
|
|
6f2281c1a4 | ||
|
|
5538b48ebb | ||
|
|
847cdfa0bd | ||
|
|
38db0ebfe1 | ||
|
|
7b57b06ed5 | ||
|
|
9c2190980e | ||
|
|
b4c83a38aa | ||
|
|
5efc22220f | ||
|
|
0ba9ed7d31 | ||
|
|
a32a58019d | ||
|
|
367c8ad0df | ||
|
|
ea93aea4dd | ||
|
|
e05428e01d | ||
|
|
24de9d7add | ||
|
|
4e136d9c55 | ||
|
|
296607fb76 | ||
|
|
544e11b628 | ||
|
|
75b195bdc0 | ||
|
|
07042d9908 | ||
|
|
2d1a13ab0a | ||
|
|
7fde146edd | ||
|
|
5f0968e348 | ||
|
|
20935e7860 | ||
|
|
40ea41996f | ||
|
|
f0fab488a5 | ||
|
|
7f2df8b886 | ||
|
|
9b33f20eaa | ||
|
|
7242583f13 | ||
|
|
229692255f | ||
|
|
96a5753b1b | ||
|
|
7b45c8b6fa | ||
|
|
f2d7e119a5 | ||
|
|
4baf78c79e | ||
|
|
d517f94c49 | ||
|
|
43ff07af3e | ||
|
|
aeca68fd56 | ||
|
|
29a24aa62e | ||
|
|
4be725b4c2 | ||
|
|
c592753182 | ||
|
|
174be4adc7 | ||
|
|
388b9dfe59 | ||
|
|
e4ec845bd4 | ||
|
|
e96d885114 | ||
|
|
eb70d3733d | ||
|
|
fcda48513a | ||
|
|
abac174e2e | ||
|
|
457dc4b279 | ||
|
|
3b2f91cd32 | ||
|
|
19f318679f | ||
|
|
d38c07a206 | ||
|
|
3b2bbbdbc4 | ||
|
|
832107f084 | ||
|
|
b23a6330f1 | ||
|
|
8970352cdd |
@@ -88,3 +88,6 @@ And more like:
|
|||||||
```
|
```
|
||||||
https://learning.edx.org/course/course-v1:edX+DemoX.1+2T2019/Being_Social/Teams
|
https://learning.edx.org/course/course-v1:edX+DemoX.1+2T2019/Being_Social/Teams
|
||||||
```
|
```
|
||||||
|
|
||||||
|
_This further work has been expanded upon in
|
||||||
|
[ADR #9: Courseware URL shortening](./0009-courseware-url-shortening.md)._
|
||||||
|
|||||||
58
docs/decisions/0009-courseware-url-shortening.md
Normal file
58
docs/decisions/0009-courseware-url-shortening.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Courseware URL shortening
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
_This updates some of the content in [ADR #8: Liberal courseware path handling](./0008-liberal-courseware-path-handling.md)._
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The current URL is not human-readable. The URL is composed of the UsageKeys for the current sequence and unit. We can't make UsageKeys themselves more readable because they're tied to student state.
|
||||||
|
|
||||||
|
This is what the URLs currently look like:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
https://learning.edx.org/course/course-v1:edX+DemoX.1+2T2019/block-v1:edX+DemoX.1+2T2019+type@sequential+block@e0a61b3d5a2046949e76d12cac5df493/block-v1:edX+DemoX.1+2T2019+type@vertical+block@52dbad5a28e140f291a476f92ce11996
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
After exploring different URL patterns and possible redundancies in the current URL format, the following key points were noticed. The course, run, and organization are stated in every portion of the URL. We also do not need the URL to tell us the type of block since it has been determined that all URLs will follow the path` /course/:courseId/:sequenceId/:unitId`.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
The courseware URL will format to the following structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
https://learning.edx.org/c/:courseId/:sequenceHash/:unitHash/:sectionSlug/:sequenceSlug/:unitSlug/
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Example URL:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
https://learning.edx.org/c/course-v1:edX+DemoX.1+2T2019/YmxvY2/njuRCq/optional-example-problem-types/stem-problems/code-grader
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The fields definition and requirements ar as follows:
|
||||||
|
|
||||||
|
* :courseId (required) - same as the previous `courseId`.
|
||||||
|
* :sequenceHash (required) - a `blake2b` version of the `sequenceId`'s `urlsafe_b64encode` .
|
||||||
|
* :unitHash (required) - a `blake2b` version of the `unitId`'s `urlsafe_b64encode`.
|
||||||
|
* :sectionSlug (optional) - `display_name` of the current sequence's parent section.
|
||||||
|
* :sequenceSlug (optional) - `display_name` of the current sequence.
|
||||||
|
* :unitSlug (optional) - `display_name` of the current unit
|
||||||
|
|
||||||
|
Partial paths will update with the required parameters as dicussed in [ADR #8: Liberal courseware path handling](./0008-liberal-courseware-path-handling.md). The `sequenceHash` and `unitHash` will shorten their respective ids using `hashlib.blake2b` with `digest_size` of 6 bytes. `Blake2b` will reduce the length of the id so the encoded version can also be short. Hashing will be handled by `blake2b` because it is the fastest hashing function in the `hashlib` library. The hash will be generated and mapped in LMS. The slugs based on `display_name` are optional because not all blocks have an associated `display_name` attributes, most likely to occur in OLX imports. The `display_name` will be pulled from the current section, sequence, and unit attribute, and if there is not an attribute `display`, the url will use the attribute `display_name_with_default`. The `display_name` will be formatted safely for a url using Django's [slugify](https://docs.djangoproject.com/en/3.2/ref/utils/#django.utils.text.slugify). Slugify allows unicode identifiers in the slug. If the slugs are omitted, it will redirect to the canonical version without the slugs.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
If old URLs are not properly routed then the content and those links will no longer be accessible to the user. The old URLs could include, but not limited to, bookmarks and exams.
|
||||||
|
|
||||||
|
## Further work
|
||||||
|
|
||||||
|
At some point, we may decide to further extend the URL shortening to the entire platform. At the moment, the hashes for the sequences and units are generated when the sequences and units are being called. In the future, it would be better if the hashes would be generated and stored when the sequences and units are originally created. This would require `learning_sequences` to include a class for unit storage, which is not being stored at the moment.
|
||||||
@@ -66,4 +66,5 @@ Factory.define('outlineTabData')
|
|||||||
handouts_html: '<ul><li>Handout 1</li></ul>',
|
handouts_html: '<ul><li>Handout 1</li></ul>',
|
||||||
offer: null,
|
offer: null,
|
||||||
welcome_message_html: '<p>Welcome to this course!</p>',
|
welcome_message_html: '<p>Welcome to this course!</p>',
|
||||||
|
mfe_short_url_is_active: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Object {
|
|||||||
"proctoredExamsEnabledWaffleFlag": false,
|
"proctoredExamsEnabledWaffleFlag": false,
|
||||||
"sequenceId": null,
|
"sequenceId": null,
|
||||||
"sequenceStatus": "loading",
|
"sequenceStatus": "loading",
|
||||||
|
"shortLinkFeatureFlag": false,
|
||||||
"specialExamsEnabledWaffleFlag": false,
|
"specialExamsEnabledWaffleFlag": false,
|
||||||
},
|
},
|
||||||
"models": Object {
|
"models": Object {
|
||||||
@@ -321,6 +322,7 @@ Object {
|
|||||||
"proctoredExamsEnabledWaffleFlag": false,
|
"proctoredExamsEnabledWaffleFlag": false,
|
||||||
"sequenceId": null,
|
"sequenceId": null,
|
||||||
"sequenceStatus": "loading",
|
"sequenceStatus": "loading",
|
||||||
|
"shortLinkFeatureFlag": false,
|
||||||
"specialExamsEnabledWaffleFlag": false,
|
"specialExamsEnabledWaffleFlag": false,
|
||||||
},
|
},
|
||||||
"models": Object {
|
"models": Object {
|
||||||
@@ -423,6 +425,7 @@ Object {
|
|||||||
"due": null,
|
"due": null,
|
||||||
"effortActivities": 2,
|
"effortActivities": 2,
|
||||||
"effortTime": 15,
|
"effortTime": 15,
|
||||||
|
"hash_key": "abcdabcd1",
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
||||||
"legacyWebUrl": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1?experience=legacy",
|
"legacyWebUrl": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1?experience=legacy",
|
||||||
@@ -471,6 +474,7 @@ Object {
|
|||||||
"hasVisitedCourse": false,
|
"hasVisitedCourse": false,
|
||||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
|
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
|
||||||
},
|
},
|
||||||
|
"shortLinkFeatureFlag": true,
|
||||||
"timeOffsetMillis": 0,
|
"timeOffsetMillis": 0,
|
||||||
"userHasPassingGrade": undefined,
|
"userHasPassingGrade": undefined,
|
||||||
"verifiedMode": Object {
|
"verifiedMode": Object {
|
||||||
@@ -507,6 +511,7 @@ Object {
|
|||||||
"proctoredExamsEnabledWaffleFlag": false,
|
"proctoredExamsEnabledWaffleFlag": false,
|
||||||
"sequenceId": null,
|
"sequenceId": null,
|
||||||
"sequenceStatus": "loading",
|
"sequenceStatus": "loading",
|
||||||
|
"shortLinkFeatureFlag": false,
|
||||||
"specialExamsEnabledWaffleFlag": false,
|
"specialExamsEnabledWaffleFlag": false,
|
||||||
},
|
},
|
||||||
"models": Object {
|
"models": Object {
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ export function normalizeOutlineBlocks(courseId, blocks) {
|
|||||||
// link to the MFE ourselves).
|
// link to the MFE ourselves).
|
||||||
showLink: !!block.legacy_web_url,
|
showLink: !!block.legacy_web_url,
|
||||||
title: block.display_name,
|
title: block.display_name,
|
||||||
|
hash_key: block.hash_key,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -353,6 +354,7 @@ export async function getOutlineTabData(courseId) {
|
|||||||
const userHasPassingGrade = data.user_has_passing_grade;
|
const userHasPassingGrade = data.user_has_passing_grade;
|
||||||
const verifiedMode = camelCaseObject(data.verified_mode);
|
const verifiedMode = camelCaseObject(data.verified_mode);
|
||||||
const welcomeMessageHtml = data.welcome_message_html;
|
const welcomeMessageHtml = data.welcome_message_html;
|
||||||
|
const shortLinkFeatureFlag = data.mfe_short_url_is_active;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessExpiration,
|
accessExpiration,
|
||||||
@@ -374,6 +376,7 @@ export async function getOutlineTabData(courseId) {
|
|||||||
userHasPassingGrade,
|
userHasPassingGrade,
|
||||||
verifiedMode,
|
verifiedMode,
|
||||||
welcomeMessageHtml,
|
welcomeMessageHtml,
|
||||||
|
shortLinkFeatureFlag,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ describe('DatesTab', () => {
|
|||||||
component = (
|
component = (
|
||||||
<AppProvider store={store}>
|
<AppProvider store={store}>
|
||||||
<UserMessagesProvider>
|
<UserMessagesProvider>
|
||||||
<Route path="/course/:courseId/dates">
|
<Route path="/c/:courseId/dates">
|
||||||
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
|
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
|
||||||
<DatesTab />
|
<DatesTab />
|
||||||
</TabContainer>
|
</TabContainer>
|
||||||
@@ -81,7 +81,7 @@ describe('DatesTab', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
||||||
axiosMock.onGet(datesUrl).reply(200, datesTabData);
|
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);
|
render(component);
|
||||||
});
|
});
|
||||||
@@ -147,7 +147,7 @@ describe('DatesTab', () => {
|
|||||||
describe('Suggested schedule messaging', () => {
|
describe('Suggested schedule messaging', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setMetadata({ is_self_paced: true, is_enrolled: true });
|
setMetadata({ is_self_paced: true, is_enrolled: true });
|
||||||
history.push(`/course/${courseId}/dates`);
|
history.push(`/c/${courseId}/dates`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders SuggestedScheduleHeader', async () => {
|
it('renders SuggestedScheduleHeader', async () => {
|
||||||
@@ -316,7 +316,7 @@ describe('DatesTab', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
axiosMock.onGet(datesUrl).reply(200, datesTabData);
|
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 () => {
|
it('redirects to course survey for a survey_required error code', async () => {
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ describe('Outline Tab', () => {
|
|||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
|
|
||||||
const sequenceLink = screen.getByText('Title of Sequence');
|
const sequenceLink = screen.getByText('Title of Sequence');
|
||||||
expect(sequenceLink.getAttribute('href')).toContain(`/course/${courseId}`);
|
expect(sequenceLink.getAttribute('href')).toContain(`/c/${courseId}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ function Section({
|
|||||||
courseBlocks: {
|
courseBlocks: {
|
||||||
sequences,
|
sequences,
|
||||||
},
|
},
|
||||||
|
shortLinkFeatureFlag,
|
||||||
} = useModel('outline', courseId);
|
} = useModel('outline', courseId);
|
||||||
|
|
||||||
const [open, setOpen] = useState(defaultOpen);
|
const [open, setOpen] = useState(defaultOpen);
|
||||||
@@ -39,6 +40,28 @@ function Section({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOpen(defaultOpen);
|
setOpen(defaultOpen);
|
||||||
}, []);
|
}, []);
|
||||||
|
let sequenceLinks;
|
||||||
|
if (shortLinkFeatureFlag) {
|
||||||
|
sequenceLinks = sequenceIds.map((sequenceId, index) => (
|
||||||
|
<SequenceLink
|
||||||
|
key={sequenceId}
|
||||||
|
id={sequences[sequenceId].hash_key}
|
||||||
|
courseId={courseId}
|
||||||
|
sequence={sequences[sequenceId]}
|
||||||
|
first={index === 0}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
sequenceLinks = sequenceIds.map((sequenceId, index) => (
|
||||||
|
<SequenceLink
|
||||||
|
key={sequenceId}
|
||||||
|
id={sequenceId}
|
||||||
|
courseId={courseId}
|
||||||
|
sequence={sequences[sequenceId]}
|
||||||
|
first={index === 0}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
const sectionTitle = (
|
const sectionTitle = (
|
||||||
<div className="row w-100 m-0">
|
<div className="row w-100 m-0">
|
||||||
@@ -96,15 +119,7 @@ function Section({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ol className="list-unstyled">
|
<ol className="list-unstyled">
|
||||||
{sequenceIds.map((sequenceId, index) => (
|
{sequenceLinks}
|
||||||
<SequenceLink
|
|
||||||
key={sequenceId}
|
|
||||||
id={sequenceId}
|
|
||||||
courseId={courseId}
|
|
||||||
sequence={sequences[sequenceId]}
|
|
||||||
first={index === 0}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ol>
|
</ol>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ function SequenceLink({
|
|||||||
// canLoadCourseware is true if the Courseware MFE is enabled, false otherwise
|
// canLoadCourseware is true if the Courseware MFE is enabled, false otherwise
|
||||||
const coursewareUrl = (
|
const coursewareUrl = (
|
||||||
canLoadCourseware
|
canLoadCourseware
|
||||||
? <Link to={`/course/${courseId}/${id}`}>{title}</Link>
|
? <Link to={`/c/${courseId}/${id}`}>{title}</Link>
|
||||||
: <Hyperlink destination={legacyWebUrl}>{title}</Hyperlink>
|
: <Hyperlink destination={legacyWebUrl}>{title}</Hyperlink>
|
||||||
);
|
);
|
||||||
const displayTitle = showLink ? coursewareUrl : title;
|
const displayTitle = showLink ? coursewareUrl : title;
|
||||||
|
|||||||
@@ -17,15 +17,21 @@ import { TabPage } from '../tab-page';
|
|||||||
import Course from './course';
|
import Course from './course';
|
||||||
import { handleNextSectionCelebration } from './course/celebration';
|
import { handleNextSectionCelebration } from './course/celebration';
|
||||||
|
|
||||||
|
const checkUrlLength = memoize((shortLinkFeatureFlag, courseStatus, courseId, sequence, unitHashKey) => {
|
||||||
|
if (shortLinkFeatureFlag && courseStatus === 'loaded' && sequence && unitHashKey) {
|
||||||
|
history.replace(`/c/${courseId}/${sequence.hash_key}/${unitHashKey}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const checkResumeRedirect = memoize((courseStatus, courseId, sequenceId, firstSequenceId) => {
|
const checkResumeRedirect = memoize((courseStatus, courseId, sequenceId, firstSequenceId) => {
|
||||||
if (courseStatus === 'loaded' && !sequenceId) {
|
if (courseStatus === 'loaded' && !sequenceId) {
|
||||||
// Note that getResumeBlock is just an API call, not a redux thunk.
|
// Note that getResumeBlock is just an API call, not a redux thunk.
|
||||||
getResumeBlock(courseId).then((data) => {
|
getResumeBlock(courseId).then((data) => {
|
||||||
// This is a replace because we don't want this change saved in the browser's history.
|
// This is a replace because we don't want this change saved in the browser's history.
|
||||||
if (data.sectionId && data.unitId) {
|
if (data.sectionId && data.unitId) {
|
||||||
history.replace(`/course/${courseId}/${data.sectionId}/${data.unitId}`);
|
history.replace(`/c/${courseId}/${data.sectionId}/${data.unitId}`);
|
||||||
} else if (firstSequenceId) {
|
} else if (firstSequenceId) {
|
||||||
history.replace(`/course/${courseId}/${firstSequenceId}`);
|
history.replace(`/c/${courseId}/${firstSequenceId}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -33,7 +39,7 @@ const checkResumeRedirect = memoize((courseStatus, courseId, sequenceId, firstSe
|
|||||||
|
|
||||||
const checkSectionUnitToUnitRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId) => {
|
const checkSectionUnitToUnitRedirect = memoize((courseStatus, courseId, sequenceStatus, section, unitId) => {
|
||||||
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && unitId) {
|
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && unitId) {
|
||||||
history.replace(`/course/${courseId}/${unitId}`);
|
history.replace(`/c/${courseId}/${unitId}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,10 +47,10 @@ const checkSectionToSequenceRedirect = memoize((courseStatus, courseId, sequence
|
|||||||
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && !unitId) {
|
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && section && !unitId) {
|
||||||
// If the section is non-empty, redirect to its first sequence.
|
// If the section is non-empty, redirect to its first sequence.
|
||||||
if (section.sequenceIds && section.sequenceIds[0]) {
|
if (section.sequenceIds && section.sequenceIds[0]) {
|
||||||
history.replace(`/course/${courseId}/${section.sequenceIds[0]}`);
|
history.replace(`/c/${courseId}/${section.sequenceIds[0]}`);
|
||||||
// Otherwise, just go to the course root, letting the resume redirect take care of things.
|
// Otherwise, just go to the course root, letting the resume redirect take care of things.
|
||||||
} else {
|
} else {
|
||||||
history.replace(`/course/${courseId}`);
|
history.replace(`/c/${courseId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -53,7 +59,7 @@ const checkUnitToSequenceUnitRedirect = memoize((courseStatus, courseId, sequenc
|
|||||||
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && unit) {
|
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && unit) {
|
||||||
// If the sequence failed to load as a sequence, but it *did* load as a unit, then
|
// If the sequence failed to load as a sequence, but it *did* load as a unit, then
|
||||||
// insert the unit's parent sequenceId into the URL.
|
// insert the unit's parent sequenceId into the URL.
|
||||||
history.replace(`/course/${courseId}/${unit.sequenceId}/${unit.id}`);
|
history.replace(`/c/${courseId}/${unit.sequenceId}/${unit.id}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,7 +78,7 @@ const checkSequenceToSequenceUnitRedirect = memoize((courseId, sequenceStatus, s
|
|||||||
if (sequence.unitIds !== undefined && sequence.unitIds.length > 0) {
|
if (sequence.unitIds !== undefined && sequence.unitIds.length > 0) {
|
||||||
const nextUnitId = sequence.unitIds[sequence.activeUnitIndex];
|
const nextUnitId = sequence.unitIds[sequence.activeUnitIndex];
|
||||||
// This is a replace because we don't want this change saved in the browser's history.
|
// This is a replace because we don't want this change saved in the browser's history.
|
||||||
history.replace(`/course/${courseId}/${sequence.id}/${nextUnitId}`);
|
history.replace(`/c/${courseId}/${sequence.id}/${nextUnitId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -106,13 +112,13 @@ class CoursewareContainer extends Component {
|
|||||||
match: {
|
match: {
|
||||||
params: {
|
params: {
|
||||||
courseId: routeCourseId,
|
courseId: routeCourseId,
|
||||||
sequenceId: routeSequenceId,
|
sequenceId: routeSequenceHash,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} = this.props;
|
} = this.props;
|
||||||
// Load data whenever the course or sequence ID changes.
|
// Load data whenever the course or sequence ID changes.
|
||||||
this.checkFetchCourse(routeCourseId);
|
this.checkFetchCourse(routeCourseId);
|
||||||
this.checkFetchSequence(routeSequenceId);
|
this.checkFetchSequence(routeSequenceHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
@@ -127,18 +133,28 @@ class CoursewareContainer extends Component {
|
|||||||
firstSequenceId,
|
firstSequenceId,
|
||||||
unitViaSequenceId,
|
unitViaSequenceId,
|
||||||
sectionViaSequenceId,
|
sectionViaSequenceId,
|
||||||
|
unitIdHashKeyMap,
|
||||||
|
shortLinkFeatureFlag,
|
||||||
match: {
|
match: {
|
||||||
params: {
|
params: {
|
||||||
courseId: routeCourseId,
|
courseId: routeCourseId,
|
||||||
sequenceId: routeSequenceId,
|
sequenceId: routeSequenceHash,
|
||||||
unitId: routeUnitId,
|
unitId: routeUnitId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// Load data whenever the course or sequence ID changes.
|
// Load data whenever the course or sequence ID changes.
|
||||||
this.checkFetchCourse(routeCourseId);
|
this.checkFetchCourse(routeCourseId);
|
||||||
this.checkFetchSequence(routeSequenceId);
|
this.checkFetchSequence(routeSequenceHash);
|
||||||
|
if (sequence && routeSequenceHash.includes('block') && unitIdHashKeyMap) {
|
||||||
|
let unitHashKey;
|
||||||
|
Object.values(unitIdHashKeyMap).forEach(id => {
|
||||||
|
if (id === routeUnitId) {
|
||||||
|
unitHashKey = Object.keys(unitIdHashKeyMap).find(key => unitIdHashKeyMap[key] === id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
checkUrlLength(shortLinkFeatureFlag, courseStatus, courseId, sequence, unitHashKey);
|
||||||
|
}
|
||||||
|
|
||||||
// All courseware URLs should normalize to the format /course/:courseId/:sequenceId/:unitId
|
// All courseware URLs should normalize to the format /course/:courseId/:sequenceId/:unitId
|
||||||
// via the series of redirection rules below.
|
// via the series of redirection rules below.
|
||||||
@@ -201,7 +217,7 @@ class CoursewareContainer extends Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
this.props.checkBlockCompletion(courseId, sequenceId, routeUnitId);
|
this.props.checkBlockCompletion(courseId, sequenceId, routeUnitId);
|
||||||
history.push(`/course/${courseId}/${sequenceId}/${nextUnitId}`);
|
history.push(`/c/${courseId}/${sequenceId}/${nextUnitId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNextSequenceClick = () => {
|
handleNextSequenceClick = () => {
|
||||||
@@ -211,16 +227,20 @@ class CoursewareContainer extends Component {
|
|||||||
nextSequence,
|
nextSequence,
|
||||||
sequence,
|
sequence,
|
||||||
sequenceId,
|
sequenceId,
|
||||||
|
shortLinkFeatureFlag,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (nextSequence !== null) {
|
if (nextSequence !== null) {
|
||||||
|
let nextSequenceParam = nextSequence.id;
|
||||||
|
if (shortLinkFeatureFlag) {
|
||||||
|
nextSequenceParam = nextSequence.hash_key;
|
||||||
|
}
|
||||||
let nextUnitId = null;
|
let nextUnitId = null;
|
||||||
if (nextSequence.unitIds.length > 0) {
|
if (nextSequence.unitIds.length > 0) {
|
||||||
[nextUnitId] = nextSequence.unitIds;
|
[nextUnitId] = nextSequence.unitIds;
|
||||||
history.push(`/course/${courseId}/${nextSequence.id}/${nextUnitId}`);
|
history.push(`/c/${courseId}/${nextSequenceParam}/${nextUnitId}`);
|
||||||
} else {
|
} else {
|
||||||
// Some sequences have no units. This will show a blank page with prev/next buttons.
|
// Some sequences have no units. This will show a blank page with prev/next buttons.
|
||||||
history.push(`/course/${courseId}/${nextSequence.id}`);
|
history.push(`/c/${courseId}/${nextSequenceParam}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const celebrateFirstSection = course && course.celebrations && course.celebrations.firstSection;
|
const celebrateFirstSection = course && course.celebrations && course.celebrations.firstSection;
|
||||||
@@ -231,14 +251,22 @@ class CoursewareContainer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlePreviousSequenceClick = () => {
|
handlePreviousSequenceClick = () => {
|
||||||
const { previousSequence, courseId } = this.props;
|
const {
|
||||||
|
previousSequence,
|
||||||
|
courseId,
|
||||||
|
shortLinkFeatureFlag,
|
||||||
|
} = this.props;
|
||||||
if (previousSequence !== null) {
|
if (previousSequence !== null) {
|
||||||
|
let previousSequenceParam = previousSequence.id;
|
||||||
|
if (shortLinkFeatureFlag) {
|
||||||
|
previousSequenceParam = previousSequence.hash_key;
|
||||||
|
}
|
||||||
if (previousSequence.unitIds.length > 0) {
|
if (previousSequence.unitIds.length > 0) {
|
||||||
const previousUnitId = previousSequence.unitIds[previousSequence.unitIds.length - 1];
|
const previousUnitId = previousSequence.unitIds[previousSequence.unitIds.length - 1];
|
||||||
history.push(`/course/${courseId}/${previousSequence.id}/${previousUnitId}`);
|
history.push(`/c/${courseId}/${previousSequenceParam}/${previousUnitId}`);
|
||||||
} else {
|
} else {
|
||||||
// Some sequences have no units. This will show a blank page with prev/next buttons.
|
// Some sequences have no units. This will show a blank page with prev/next buttons.
|
||||||
history.push(`/course/${courseId}/${previousSequence.id}`);
|
history.push(`/c/${courseId}/${previousSequenceParam}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,6 +276,9 @@ class CoursewareContainer extends Component {
|
|||||||
courseStatus,
|
courseStatus,
|
||||||
courseId,
|
courseId,
|
||||||
sequenceId,
|
sequenceId,
|
||||||
|
sequence,
|
||||||
|
shortLinkFeatureFlag,
|
||||||
|
unitIdHashKeyMap,
|
||||||
match: {
|
match: {
|
||||||
params: {
|
params: {
|
||||||
unitId: routeUnitId,
|
unitId: routeUnitId,
|
||||||
@@ -255,18 +286,30 @@ class CoursewareContainer extends Component {
|
|||||||
},
|
},
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
// This helps process old URLS that still use a blocks usage key in the URL.
|
||||||
|
let updatedSequenceId;
|
||||||
|
let updatedUnitId;
|
||||||
|
if (shortLinkFeatureFlag && sequence) {
|
||||||
|
if (!sequenceId.includes('block')) {
|
||||||
|
updatedSequenceId = sequence.id;
|
||||||
|
}
|
||||||
|
if (routeUnitId && !routeUnitId.includes('block')) {
|
||||||
|
updatedUnitId = unitIdHashKeyMap[routeUnitId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabPage
|
<TabPage
|
||||||
activeTabSlug="courseware"
|
activeTabSlug="courseware"
|
||||||
courseId={courseId}
|
courseId={courseId}
|
||||||
unitId={routeUnitId}
|
unitId={updatedUnitId || routeUnitId}
|
||||||
courseStatus={courseStatus}
|
courseStatus={courseStatus}
|
||||||
metadataModel="coursewareMeta"
|
metadataModel="coursewareMeta"
|
||||||
>
|
>
|
||||||
<Course
|
<Course
|
||||||
courseId={courseId}
|
courseId={courseId}
|
||||||
sequenceId={sequenceId}
|
sequenceId={updatedSequenceId || sequenceId}
|
||||||
unitId={routeUnitId}
|
unitId={updatedUnitId || routeUnitId}
|
||||||
nextSequenceHandler={this.handleNextSequenceClick}
|
nextSequenceHandler={this.handleNextSequenceClick}
|
||||||
previousSequenceHandler={this.handlePreviousSequenceClick}
|
previousSequenceHandler={this.handlePreviousSequenceClick}
|
||||||
unitNavigationHandler={this.handleUnitNavigationClick}
|
unitNavigationHandler={this.handleUnitNavigationClick}
|
||||||
@@ -285,6 +328,7 @@ const sequenceShape = PropTypes.shape({
|
|||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
unitIds: PropTypes.arrayOf(PropTypes.string).isRequired,
|
unitIds: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
sectionId: PropTypes.string.isRequired,
|
sectionId: PropTypes.string.isRequired,
|
||||||
|
hash_key: PropTypes.string.isRequired,
|
||||||
isTimeLimited: PropTypes.bool,
|
isTimeLimited: PropTypes.bool,
|
||||||
isProctored: PropTypes.bool,
|
isProctored: PropTypes.bool,
|
||||||
legacyWebUrl: PropTypes.string,
|
legacyWebUrl: PropTypes.string,
|
||||||
@@ -318,6 +362,7 @@ CoursewareContainer.propTypes = {
|
|||||||
previousSequence: sequenceShape,
|
previousSequence: sequenceShape,
|
||||||
unitViaSequenceId: unitShape,
|
unitViaSequenceId: unitShape,
|
||||||
sectionViaSequenceId: sectionShape,
|
sectionViaSequenceId: sectionShape,
|
||||||
|
unitIdHashKeyMap: unitShape,
|
||||||
course: courseShape,
|
course: courseShape,
|
||||||
sequence: sequenceShape,
|
sequence: sequenceShape,
|
||||||
saveSequencePosition: PropTypes.func.isRequired,
|
saveSequencePosition: PropTypes.func.isRequired,
|
||||||
@@ -326,6 +371,7 @@ CoursewareContainer.propTypes = {
|
|||||||
fetchSequence: PropTypes.func.isRequired,
|
fetchSequence: PropTypes.func.isRequired,
|
||||||
specialExamsEnabledWaffleFlag: PropTypes.bool.isRequired,
|
specialExamsEnabledWaffleFlag: PropTypes.bool.isRequired,
|
||||||
proctoredExamsEnabledWaffleFlag: PropTypes.bool.isRequired,
|
proctoredExamsEnabledWaffleFlag: PropTypes.bool.isRequired,
|
||||||
|
shortLinkFeatureFlag: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
CoursewareContainer.defaultProps = {
|
CoursewareContainer.defaultProps = {
|
||||||
@@ -338,6 +384,7 @@ CoursewareContainer.defaultProps = {
|
|||||||
sectionViaSequenceId: null,
|
sectionViaSequenceId: null,
|
||||||
course: null,
|
course: null,
|
||||||
sequence: null,
|
sequence: null,
|
||||||
|
unitIdHashKeyMap: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentCourseSelector = createSelector(
|
const currentCourseSelector = createSelector(
|
||||||
@@ -349,7 +396,16 @@ const currentCourseSelector = createSelector(
|
|||||||
const currentSequenceSelector = createSelector(
|
const currentSequenceSelector = createSelector(
|
||||||
(state) => state.models.sequences || {},
|
(state) => state.models.sequences || {},
|
||||||
(state) => state.courseware.sequenceId,
|
(state) => state.courseware.sequenceId,
|
||||||
(sequencesById, sequenceId) => (sequencesById[sequenceId] ? sequencesById[sequenceId] : null),
|
(state) => state.models.sequenceIdToHashKeyMap,
|
||||||
|
(sequencesById, sequenceId, sequenceMap) => {
|
||||||
|
if (!sequencesById[sequenceId] && Object.keys(sequencesById).length > 0 && sequenceMap) {
|
||||||
|
if (sequenceId in sequenceMap) {
|
||||||
|
const updatedSequenceId = sequenceMap[sequenceId];
|
||||||
|
return sequencesById[updatedSequenceId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sequencesById[sequenceId] ? sequencesById[sequenceId] : null;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const sequenceIdsSelector = createSelector(
|
const sequenceIdsSelector = createSelector(
|
||||||
@@ -369,11 +425,18 @@ const previousSequenceSelector = createSelector(
|
|||||||
sequenceIdsSelector,
|
sequenceIdsSelector,
|
||||||
(state) => state.models.sequences || {},
|
(state) => state.models.sequences || {},
|
||||||
(state) => state.courseware.sequenceId,
|
(state) => state.courseware.sequenceId,
|
||||||
(sequenceIds, sequencesById, sequenceId) => {
|
(state) => state.models.sequenceIdToHashKeyMap,
|
||||||
|
(sequenceIds, sequencesById, sequenceId, sequenceMap) => {
|
||||||
if (!sequenceId || sequenceIds.length === 0) {
|
if (!sequenceId || sequenceIds.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const sequenceIndex = sequenceIds.indexOf(sequenceId);
|
let sequenceIndex = sequenceIds.indexOf(sequenceId);
|
||||||
|
if (!sequencesById[sequenceId] && Object.keys(sequencesById).length > 0 && sequenceMap) {
|
||||||
|
if (sequenceId in sequenceMap) {
|
||||||
|
const updatedSequenceId = sequenceMap[sequenceId];
|
||||||
|
sequenceIndex = sequenceIds.indexOf(updatedSequenceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
const previousSequenceId = sequenceIndex > 0 ? sequenceIds[sequenceIndex - 1] : null;
|
const previousSequenceId = sequenceIndex > 0 ? sequenceIds[sequenceIndex - 1] : null;
|
||||||
return previousSequenceId !== null ? sequencesById[previousSequenceId] : null;
|
return previousSequenceId !== null ? sequencesById[previousSequenceId] : null;
|
||||||
},
|
},
|
||||||
@@ -383,11 +446,18 @@ const nextSequenceSelector = createSelector(
|
|||||||
sequenceIdsSelector,
|
sequenceIdsSelector,
|
||||||
(state) => state.models.sequences || {},
|
(state) => state.models.sequences || {},
|
||||||
(state) => state.courseware.sequenceId,
|
(state) => state.courseware.sequenceId,
|
||||||
(sequenceIds, sequencesById, sequenceId) => {
|
(state) => state.models.sequenceIdToHashKeyMap,
|
||||||
|
(sequenceIds, sequencesById, sequenceId, sequenceMap) => {
|
||||||
if (!sequenceId || sequenceIds.length === 0) {
|
if (!sequenceId || sequenceIds.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const sequenceIndex = sequenceIds.indexOf(sequenceId);
|
let sequenceIndex = sequenceIds.indexOf(sequenceId);
|
||||||
|
if (!sequencesById[sequenceId] && Object.keys(sequencesById).length > 0 && sequenceMap) {
|
||||||
|
if (sequenceId in sequenceMap) {
|
||||||
|
const updatedSequenceId = sequenceMap[sequenceId];
|
||||||
|
sequenceIndex = sequenceIds.indexOf(updatedSequenceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
const nextSequenceId = sequenceIndex < sequenceIds.length - 1 ? sequenceIds[sequenceIndex + 1] : null;
|
const nextSequenceId = sequenceIndex < sequenceIds.length - 1 ? sequenceIds[sequenceIndex + 1] : null;
|
||||||
return nextSequenceId !== null ? sequencesById[nextSequenceId] : null;
|
return nextSequenceId !== null ? sequencesById[nextSequenceId] : null;
|
||||||
},
|
},
|
||||||
@@ -420,7 +490,21 @@ const sectionViaSequenceIdSelector = createSelector(
|
|||||||
const unitViaSequenceIdSelector = createSelector(
|
const unitViaSequenceIdSelector = createSelector(
|
||||||
(state) => state.models.units || {},
|
(state) => state.models.units || {},
|
||||||
(state) => state.courseware.sequenceId,
|
(state) => state.courseware.sequenceId,
|
||||||
(unitsById, sequenceId) => (unitsById[sequenceId] ? unitsById[sequenceId] : null),
|
(state) => state.models.unitIdHashKeyMap,
|
||||||
|
(unitsById, sequenceId, unitMap) => {
|
||||||
|
if (!unitsById[sequenceId] && Object.keys(unitsById).length > 0 && unitMap) {
|
||||||
|
if (sequenceId in unitMap) {
|
||||||
|
const updatedSequenceId = unitMap[sequenceId];
|
||||||
|
return unitsById[updatedSequenceId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unitsById[sequenceId] ? unitsById[sequenceId] : null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const unitIdHashKeyMapSelector = createSelector(
|
||||||
|
(state) => state.models.unitIdToHashKeyMap,
|
||||||
|
(unitIdToHashKeyMap) => (unitIdToHashKeyMap),
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
@@ -431,6 +515,7 @@ const mapStateToProps = (state) => {
|
|||||||
sequenceStatus,
|
sequenceStatus,
|
||||||
specialExamsEnabledWaffleFlag,
|
specialExamsEnabledWaffleFlag,
|
||||||
proctoredExamsEnabledWaffleFlag,
|
proctoredExamsEnabledWaffleFlag,
|
||||||
|
shortLinkFeatureFlag,
|
||||||
} = state.courseware;
|
} = state.courseware;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -440,6 +525,7 @@ const mapStateToProps = (state) => {
|
|||||||
sequenceStatus,
|
sequenceStatus,
|
||||||
specialExamsEnabledWaffleFlag,
|
specialExamsEnabledWaffleFlag,
|
||||||
proctoredExamsEnabledWaffleFlag,
|
proctoredExamsEnabledWaffleFlag,
|
||||||
|
shortLinkFeatureFlag,
|
||||||
course: currentCourseSelector(state),
|
course: currentCourseSelector(state),
|
||||||
sequence: currentSequenceSelector(state),
|
sequence: currentSequenceSelector(state),
|
||||||
previousSequence: previousSequenceSelector(state),
|
previousSequence: previousSequenceSelector(state),
|
||||||
@@ -447,6 +533,7 @@ const mapStateToProps = (state) => {
|
|||||||
firstSequenceId: firstSequenceIdSelector(state),
|
firstSequenceId: firstSequenceIdSelector(state),
|
||||||
sectionViaSequenceId: sectionViaSequenceIdSelector(state),
|
sectionViaSequenceId: sectionViaSequenceIdSelector(state),
|
||||||
unitViaSequenceId: unitViaSequenceIdSelector(state),
|
unitViaSequenceId: unitViaSequenceIdSelector(state),
|
||||||
|
unitIdHashKeyMap: unitIdHashKeyMapSelector(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ describe('CoursewareContainer', () => {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
path={[
|
path={[
|
||||||
'/course/:courseId/:sequenceId/:unitId',
|
'/c/:courseId/:sequenceId/:unitId',
|
||||||
'/course/:courseId/:sequenceId',
|
'/c/:courseId/:sequenceId',
|
||||||
'/course/:courseId',
|
'/c/:courseId',
|
||||||
]}
|
]}
|
||||||
component={CoursewareContainer}
|
component={CoursewareContainer}
|
||||||
/>
|
/>
|
||||||
@@ -128,8 +128,10 @@ describe('CoursewareContainer', () => {
|
|||||||
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
|
||||||
|
|
||||||
sequenceMetadatas.forEach(sequenceMetadata => {
|
sequenceMetadatas.forEach(sequenceMetadata => {
|
||||||
const sequenceMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/sequence/${sequenceMetadata.item_id}`;
|
const sequenceMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/sequence/${sequenceMetadata.hash_key}`;
|
||||||
axiosMock.onGet(sequenceMetadataUrl).reply(200, sequenceMetadata);
|
axiosMock.onGet(sequenceMetadataUrl).reply(200, sequenceMetadata);
|
||||||
|
const sequenceMetadataUrlFull = `${getConfig().LMS_BASE_URL}/api/courseware/sequence/${sequenceMetadata.item_id}`;
|
||||||
|
axiosMock.onGet(sequenceMetadataUrlFull).reply(200, sequenceMetadata);
|
||||||
const proctoredExamApiUrl = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${courseId}/content_id/${sequenceMetadata.item_id}?is_learning_mfe=true`;
|
const proctoredExamApiUrl = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${courseId}/content_id/${sequenceMetadata.item_id}?is_learning_mfe=true`;
|
||||||
axiosMock.onGet(proctoredExamApiUrl).reply(200, { exam: {}, active_attempt: {} });
|
axiosMock.onGet(proctoredExamApiUrl).reply(200, { exam: {}, active_attempt: {} });
|
||||||
});
|
});
|
||||||
@@ -144,7 +146,7 @@ describe('CoursewareContainer', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it('should initialize to show a spinner', () => {
|
it('should initialize to show a spinner', () => {
|
||||||
history.push('/course/abc123');
|
history.push('/c/abc123');
|
||||||
render(component);
|
render(component);
|
||||||
|
|
||||||
const spinner = screen.getByRole('status');
|
const spinner = screen.getByRole('status');
|
||||||
@@ -190,11 +192,11 @@ describe('CoursewareContainer', () => {
|
|||||||
|
|
||||||
it('should use the resume block repsonse to pick a unit if it contains one', async () => {
|
it('should use the resume block repsonse to pick a unit if it contains one', async () => {
|
||||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/courseware/resume/${courseId}`).reply(200, {
|
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/courseware/resume/${courseId}`).reply(200, {
|
||||||
sectionId: sequenceBlock.id,
|
sectionId: sequenceBlock.hash_key,
|
||||||
unitId: unitBlocks[1].id,
|
unitId: unitBlocks[1].hash_key,
|
||||||
});
|
});
|
||||||
|
|
||||||
history.push(`/course/${courseId}`);
|
history.push(`/c/${courseId}`);
|
||||||
const container = await loadContainer();
|
const container = await loadContainer();
|
||||||
|
|
||||||
assertLoadedHeader(container);
|
assertLoadedHeader(container);
|
||||||
@@ -202,7 +204,7 @@ describe('CoursewareContainer', () => {
|
|||||||
|
|
||||||
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
||||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
||||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(unitBlocks[1].id);
|
expect(container.querySelector('.fake-unit')).toHaveTextContent(unitBlocks[1].hash_key);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use the first sequence ID and activeUnitIndex if the resume block response is empty', async () => {
|
it('should use the first sequence ID and activeUnitIndex if the resume block response is empty', async () => {
|
||||||
@@ -217,7 +219,7 @@ describe('CoursewareContainer', () => {
|
|||||||
// Note how there is no sectionId/unitId returned in this mock response!
|
// Note how there is no sectionId/unitId returned in this mock response!
|
||||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/courseware/resume/${courseId}`).reply(200, {});
|
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/courseware/resume/${courseId}`).reply(200, {});
|
||||||
|
|
||||||
history.push(`/course/${courseId}`);
|
history.push(`/c/${courseId}`);
|
||||||
const container = await loadContainer();
|
const container = await loadContainer();
|
||||||
|
|
||||||
assertLoadedHeader(container);
|
assertLoadedHeader(container);
|
||||||
@@ -237,11 +239,11 @@ describe('CoursewareContainer', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
function setUrl(urlSequenceId, urlUnitId = null) {
|
function setUrl(urlSequenceId, urlUnitId = null) {
|
||||||
history.push(`/course/${courseId}/${urlSequenceId}/${urlUnitId || ''}`);
|
history.push(`/c/${courseId}/${urlSequenceId}/${urlUnitId || ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertLocation(container, sequenceId, unitId) {
|
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(global.location.href).toEqual(expectedUrl);
|
||||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(unitId);
|
expect(container.querySelector('.fake-unit')).toHaveTextContent(unitId);
|
||||||
}
|
}
|
||||||
@@ -257,7 +259,7 @@ describe('CoursewareContainer', () => {
|
|||||||
const container = await loadContainer();
|
const container = await loadContainer();
|
||||||
assertLoadedHeader(container);
|
assertLoadedHeader(container);
|
||||||
assertSequenceNavigation(container, 2);
|
assertSequenceNavigation(container, 2);
|
||||||
assertLocation(container, sequenceTree[1][1].id, urlUnit.id);
|
assertLocation(container, sequenceTree[1][1].hash_key, urlUnit.hash_key);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -267,7 +269,7 @@ describe('CoursewareContainer', () => {
|
|||||||
const container = await loadContainer();
|
const container = await loadContainer();
|
||||||
assertLoadedHeader(container);
|
assertLoadedHeader(container);
|
||||||
assertSequenceNavigation(container, 2);
|
assertSequenceNavigation(container, 2);
|
||||||
assertLocation(container, sequenceTree[1][0].id, unitTree[1][0][0].id);
|
assertLocation(container, sequenceTree[1][0].hash_key, unitTree[1][0][0].hash_key);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -293,14 +295,14 @@ describe('CoursewareContainer', () => {
|
|||||||
it('should ignore the section ID and instead redirect to the course root', async () => {
|
it('should ignore the section ID and instead redirect to the course root', async () => {
|
||||||
setUrl(sectionTree[1].id);
|
setUrl(sectionTree[1].id);
|
||||||
await loadContainer();
|
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 () => {
|
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.
|
// Specific unit ID used here shouldn't matter; is ignored due to empty section.
|
||||||
setUrl(sectionTree[1].id, unitTree[0][0][0]);
|
setUrl(sectionTree[1].id, unitTree[0][0][0]);
|
||||||
await loadContainer();
|
await loadContainer();
|
||||||
expect(global.location.href).toEqual(`http://localhost/course/${courseId}`);
|
expect(global.location.href).toEqual(`http://localhost/c/${courseId}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -314,15 +316,15 @@ describe('CoursewareContainer', () => {
|
|||||||
|
|
||||||
it('should insert the sequence ID into the URL', async () => {
|
it('should insert the sequence ID into the URL', async () => {
|
||||||
const unit = unitTree[1][0][1];
|
const unit = unitTree[1][0][1];
|
||||||
history.push(`/course/${courseId}/${unit.id}`);
|
history.push(`/c/${courseId}/${unit.id}`);
|
||||||
const container = await loadContainer();
|
const container = await loadContainer();
|
||||||
|
|
||||||
assertLoadedHeader(container);
|
assertLoadedHeader(container);
|
||||||
assertSequenceNavigation(container, 2);
|
assertSequenceNavigation(container, 2);
|
||||||
const expectedSequenceId = sequenceTree[1][0].id;
|
const expectedSequenceId = sequenceTree[1][0].hash_key;
|
||||||
const expectedUrl = `http://localhost/course/${courseId}/${expectedSequenceId}/${unit.id}`;
|
const expectedUrl = `http://localhost/c/${courseId}/${expectedSequenceId}/${unit.hash_key}`;
|
||||||
expect(global.location.href).toEqual(expectedUrl);
|
expect(global.location.href).toEqual(expectedUrl);
|
||||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(unit.id);
|
expect(container.querySelector('.fake-unit')).toHaveTextContent(unit.hash_key);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -331,7 +333,7 @@ describe('CoursewareContainer', () => {
|
|||||||
const unitBlocks = defaultUnitBlocks;
|
const unitBlocks = defaultUnitBlocks;
|
||||||
|
|
||||||
it('should pick the first unit if position was not defined (activeUnitIndex becomes 0)', async () => {
|
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.hash_key}`);
|
||||||
const container = await loadContainer();
|
const container = await loadContainer();
|
||||||
|
|
||||||
assertLoadedHeader(container);
|
assertLoadedHeader(container);
|
||||||
@@ -339,7 +341,7 @@ describe('CoursewareContainer', () => {
|
|||||||
|
|
||||||
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
||||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
||||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(unitBlocks[0].id);
|
expect(container.querySelector('.fake-unit')).toHaveTextContent(unitBlocks[0].hash_key);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use activeUnitIndex to pick a unit from the sequence', async () => {
|
it('should use activeUnitIndex to pick a unit from the sequence', async () => {
|
||||||
@@ -350,7 +352,7 @@ describe('CoursewareContainer', () => {
|
|||||||
);
|
);
|
||||||
setUpMockRequests({ sequenceMetadatas: [sequenceMetadata] });
|
setUpMockRequests({ sequenceMetadatas: [sequenceMetadata] });
|
||||||
|
|
||||||
history.push(`/course/${courseId}/${sequenceBlock.id}`);
|
history.push(`/c/${courseId}/${sequenceBlock.hash_key}`);
|
||||||
const container = await loadContainer();
|
const container = await loadContainer();
|
||||||
|
|
||||||
assertLoadedHeader(container);
|
assertLoadedHeader(container);
|
||||||
@@ -358,7 +360,7 @@ describe('CoursewareContainer', () => {
|
|||||||
|
|
||||||
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
||||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
||||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(unitBlocks[2].id);
|
expect(container.querySelector('.fake-unit')).toHaveTextContent(unitBlocks[2].hash_key);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -367,7 +369,7 @@ describe('CoursewareContainer', () => {
|
|||||||
const unitBlocks = defaultUnitBlocks;
|
const unitBlocks = defaultUnitBlocks;
|
||||||
|
|
||||||
it('should load the specified unit', async () => {
|
it('should load the specified unit', async () => {
|
||||||
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[2].id}`);
|
history.push(`/c/${courseId}/${sequenceBlock.hash_key}/${unitBlocks[2].hash_key}`);
|
||||||
const container = await loadContainer();
|
const container = await loadContainer();
|
||||||
|
|
||||||
assertLoadedHeader(container);
|
assertLoadedHeader(container);
|
||||||
@@ -375,7 +377,7 @@ describe('CoursewareContainer', () => {
|
|||||||
|
|
||||||
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
||||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
||||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(unitBlocks[2].id);
|
expect(container.querySelector('.fake-unit')).toHaveTextContent(unitBlocks[2].hash_key);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should navigate between units and check block completion', async () => {
|
it('should navigate between units and check block completion', async () => {
|
||||||
@@ -383,7 +385,7 @@ describe('CoursewareContainer', () => {
|
|||||||
complete: true,
|
complete: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[0].id}`);
|
history.push(`/c/${courseId}/${sequenceBlock.hash_key}/${unitBlocks[0].id}`);
|
||||||
const container = await loadContainer();
|
const container = await loadContainer();
|
||||||
|
|
||||||
const sequenceNavButtons = container.querySelectorAll('nav.sequence-navigation button');
|
const sequenceNavButtons = container.querySelectorAll('nav.sequence-navigation button');
|
||||||
@@ -391,7 +393,7 @@ describe('CoursewareContainer', () => {
|
|||||||
expect(sequenceNextButton).toHaveTextContent('Next');
|
expect(sequenceNextButton).toHaveTextContent('Next');
|
||||||
fireEvent.click(sequenceNavButtons[4]);
|
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.hash_key}/${unitBlocks[1].id}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -419,7 +421,7 @@ describe('CoursewareContainer', () => {
|
|||||||
);
|
);
|
||||||
setUpMockRequests({ sequenceMetadatas: [sequenceMetadata] });
|
setUpMockRequests({ sequenceMetadatas: [sequenceMetadata] });
|
||||||
|
|
||||||
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[2].id}`);
|
history.push(`/c/${courseId}/${sequenceBlock.hash_key}/${unitBlocks[2].hash_key}`);
|
||||||
await loadContainer();
|
await loadContainer();
|
||||||
|
|
||||||
expect(global.location.assign).toHaveBeenCalledWith(sequenceBlock.legacy_web_url);
|
expect(global.location.assign).toHaveBeenCalledWith(sequenceBlock.legacy_web_url);
|
||||||
@@ -440,7 +442,7 @@ describe('CoursewareContainer', () => {
|
|||||||
|
|
||||||
const { courseBlocks, sequenceBlocks, unitBlocks } = buildSimpleCourseBlocks(courseId, courseMetadata.name);
|
const { courseBlocks, sequenceBlocks, unitBlocks } = buildSimpleCourseBlocks(courseId, courseMetadata.name);
|
||||||
setUpMockRequests({ courseBlocks, courseMetadata });
|
setUpMockRequests({ courseBlocks, courseMetadata });
|
||||||
history.push(`/course/${courseId}/${sequenceBlocks[0].id}/${unitBlocks[0].id}`);
|
history.push(`/c/${courseId}/${sequenceBlocks[0].hash_key}/${unitBlocks[0].hash_key}`);
|
||||||
return { courseMetadata, unitBlocks };
|
return { courseMetadata, unitBlocks };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ export default () => {
|
|||||||
path={`${path}/courseware/:courseId/unit/:unitId`}
|
path={`${path}/courseware/:courseId/unit/:unitId`}
|
||||||
component={CoursewareRedirect}
|
component={CoursewareRedirect}
|
||||||
/>
|
/>
|
||||||
|
<PageRoute
|
||||||
|
path={`${path}/:courseId/:sequenceId/:unitId`}
|
||||||
|
render={({ match }) => {
|
||||||
|
global.location.assign(`/c/${match.params.courseId}/${match.params.sequenceId}/${match.params.unitId}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<PageRoute
|
<PageRoute
|
||||||
path={`${path}/course-home/:courseId`}
|
path={`${path}/course-home/:courseId`}
|
||||||
render={({ match }) => {
|
render={({ match }) => {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function CourseExit({ intl }) {
|
|||||||
} else if (mode === COURSE_EXIT_MODES.celebration) {
|
} else if (mode === COURSE_EXIT_MODES.celebration) {
|
||||||
body = (<CourseCelebration />);
|
body = (<CourseCelebration />);
|
||||||
} else {
|
} else {
|
||||||
return (<Redirect to={`/course/${courseId}`} />);
|
return (<Redirect to={`/c/${courseId}`} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ describe('Course Exit Pages', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
await fetchAndRender(<CourseExit />);
|
await fetchAndRender(<CourseExit />);
|
||||||
expect(global.location.href).toEqual(`http://localhost/course/${defaultMetadata.id}`);
|
expect(global.location.href).toEqual(`http://localhost/c/${defaultMetadata.id}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ function Sequence({
|
|||||||
|
|
||||||
const gated = sequence && sequence.gatedContent !== undefined && sequence.gatedContent.gated;
|
const gated = sequence && sequence.gatedContent !== undefined && sequence.gatedContent.gated;
|
||||||
const goToCourseExitPage = () => {
|
const goToCourseExitPage = () => {
|
||||||
history.push(`/course/${courseId}/course-end`);
|
history.push(`/c/${courseId}/course-end`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultContent = (
|
const defaultContent = (
|
||||||
|
|||||||
@@ -89,9 +89,10 @@ function Unit({
|
|||||||
/** [MM-P2P] Experiment */
|
/** [MM-P2P] Experiment */
|
||||||
mmp2p,
|
mmp2p,
|
||||||
}) {
|
}) {
|
||||||
|
const unit = useModel('units', id);
|
||||||
const { authenticatedUser } = useContext(AppContext);
|
const { authenticatedUser } = useContext(AppContext);
|
||||||
const view = authenticatedUser ? 'student_view' : 'public_view';
|
const view = authenticatedUser ? 'student_view' : 'public_view';
|
||||||
let iframeUrl = `${getConfig().LMS_BASE_URL}/xblock/${id}?show_title=0&show_bookmark_button=0&recheck_access=1&view=${view}`;
|
let iframeUrl = `${getConfig().LMS_BASE_URL}/xblock/${(unit.decoded_id || id)}?show_title=0&show_bookmark_button=0&recheck_access=1&view=${view}`;
|
||||||
if (format) {
|
if (format) {
|
||||||
iframeUrl += `&format=${format}`;
|
iframeUrl += `&format=${format}`;
|
||||||
}
|
}
|
||||||
@@ -101,7 +102,6 @@ function Unit({
|
|||||||
const [modalOptions, setModalOptions] = useState({ open: false });
|
const [modalOptions, setModalOptions] = useState({ open: false });
|
||||||
const [shouldDisplayHonorCode, setShouldDisplayHonorCode] = useState(false);
|
const [shouldDisplayHonorCode, setShouldDisplayHonorCode] = useState(false);
|
||||||
|
|
||||||
const unit = useModel('units', id);
|
|
||||||
const course = useModel('coursewareMeta', courseId);
|
const course = useModel('coursewareMeta', courseId);
|
||||||
const {
|
const {
|
||||||
contentTypeGatingEnabled,
|
contentTypeGatingEnabled,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ describe('Unit', () => {
|
|||||||
id: unit.id,
|
id: unit.id,
|
||||||
courseId: courseMetadata.id,
|
courseId: courseMetadata.id,
|
||||||
format: 'Homework',
|
format: 'Homework',
|
||||||
|
decoded_id: unit.decoded_id,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ describe('Unit', () => {
|
|||||||
const renderedUnit = screen.getByTitle(unit.display_name);
|
const renderedUnit = screen.getByTitle(unit.display_name);
|
||||||
expect(renderedUnit).toHaveAttribute('height', String(0));
|
expect(renderedUnit).toHaveAttribute('height', String(0));
|
||||||
expect(renderedUnit).toHaveAttribute(
|
expect(renderedUnit).toHaveAttribute(
|
||||||
'src', `http://localhost:18000/xblock/${mockData.id}?show_title=0&show_bookmark_button=0&recheck_access=1&view=student_view&format=${mockData.format}`,
|
'src', `http://localhost:18000/xblock/${mockData.decoded_id}?show_title=0&show_bookmark_button=0&recheck_access=1&view=student_view&format=${mockData.format}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function ContentLock({
|
|||||||
intl, courseId, prereqSectionName, prereqId, sequenceTitle,
|
intl, courseId, prereqSectionName, prereqId, sequenceTitle,
|
||||||
}) {
|
}) {
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
history.push(`/course/${courseId}/${prereqId}`);
|
history.push(`/c/${courseId}/${prereqId}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -38,6 +38,6 @@ describe('Content Lock', () => {
|
|||||||
render(<ContentLock {...mockData} />);
|
render(<ContentLock {...mockData} />);
|
||||||
fireEvent.click(screen.getByRole('button'));
|
fireEvent.click(screen.getByRole('button'));
|
||||||
|
|
||||||
expect(history.push).toHaveBeenCalledWith(`/course/${mockData.courseId}/${mockData.prereqId}`);
|
expect(history.push).toHaveBeenCalledWith(`/c/${mockData.courseId}/${mockData.prereqId}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function HonorCode({ intl, courseId }) {
|
|||||||
const siteName = getConfig().SITE_NAME;
|
const siteName = getConfig().SITE_NAME;
|
||||||
const honorCodeUrl = `${process.env.TERMS_OF_SERVICE_URL}#honor-code`;
|
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 = () => {
|
const handleAgree = () => {
|
||||||
dispatch(saveIntegritySignature(courseId));
|
dispatch(saveIntegritySignature(courseId));
|
||||||
|
|||||||
@@ -28,6 +28,6 @@ describe('Honor Code', () => {
|
|||||||
|
|
||||||
const cancelButton = screen.getByText('Cancel');
|
const cancelButton = screen.getByText('Cancel');
|
||||||
fireEvent.click(cancelButton);
|
fireEvent.click(cancelButton);
|
||||||
expect(history.push).toHaveBeenCalledWith(`/course/${mockData.courseId}/home`);
|
expect(history.push).toHaveBeenCalledWith(`/c/${mockData.courseId}/home`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,4 +58,5 @@ Factory.define('courseMetadata')
|
|||||||
is_mfe_special_exams_enabled: false,
|
is_mfe_special_exams_enabled: false,
|
||||||
is_mfe_proctored_exams_enabled: false,
|
is_mfe_proctored_exams_enabled: false,
|
||||||
recommendations: null,
|
recommendations: null,
|
||||||
|
mfe_short_url_is_active: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ Factory.define('sequenceMetadata')
|
|||||||
prereq_section_name: `${sequenceBlock.display_name}-prereq`,
|
prereq_section_name: `${sequenceBlock.display_name}-prereq`,
|
||||||
gated_section_name: sequenceBlock.display_name,
|
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(
|
.attr('items', ['unitBlocks', 'sequenceBlock'], (unitBlocks, sequenceBlock) => unitBlocks.map(
|
||||||
unitBlock => ({
|
unitBlock => ({
|
||||||
href: '',
|
href: '',
|
||||||
@@ -44,10 +47,12 @@ Factory.define('sequenceMetadata')
|
|||||||
bookmarked: unitBlock.bookmarked || false,
|
bookmarked: unitBlock.bookmarked || false,
|
||||||
path: `Chapter Display Name > ${sequenceBlock.display_name} > ${unitBlock.display_name}`,
|
path: `Chapter Display Name > ${sequenceBlock.display_name} > ${unitBlock.display_name}`,
|
||||||
type: unitBlock.type,
|
type: unitBlock.type,
|
||||||
|
hash_key: unitBlock.hash_key,
|
||||||
complete: unitBlock.complete || null,
|
complete: unitBlock.complete || null,
|
||||||
content: '',
|
content: '',
|
||||||
page_title: unitBlock.display_name,
|
page_title: unitBlock.display_name,
|
||||||
contains_content_type_gated_content: unitBlock.contains_content_type_gated_content,
|
contains_content_type_gated_content: unitBlock.contains_content_type_gated_content,
|
||||||
|
decoded_id: unitBlock.decoded_id,
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
.attrs({
|
.attrs({
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export function normalizeBlocks(courseId, blocks) {
|
|||||||
title: block.display_name,
|
title: block.display_name,
|
||||||
legacyWebUrl: block.legacy_web_url,
|
legacyWebUrl: block.legacy_web_url,
|
||||||
unitIds: block.children || [],
|
unitIds: block.children || [],
|
||||||
|
hash_key: block.hash_key,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'vertical':
|
case 'vertical':
|
||||||
@@ -46,6 +47,7 @@ export function normalizeBlocks(courseId, blocks) {
|
|||||||
id: block.id,
|
id: block.id,
|
||||||
title: block.display_name,
|
title: block.display_name,
|
||||||
legacyWebUrl: block.legacy_web_url,
|
legacyWebUrl: block.legacy_web_url,
|
||||||
|
hash_key: block.hash_key,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -87,7 +89,6 @@ export function normalizeBlocks(courseId, blocks) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +223,7 @@ function normalizeMetadata(metadata) {
|
|||||||
specialExamsEnabledWaffleFlag: data.is_mfe_special_exams_enabled,
|
specialExamsEnabledWaffleFlag: data.is_mfe_special_exams_enabled,
|
||||||
proctoredExamsEnabledWaffleFlag: data.is_mfe_proctored_exams_enabled,
|
proctoredExamsEnabledWaffleFlag: data.is_mfe_proctored_exams_enabled,
|
||||||
isMasquerading: data.original_user_is_staff && !data.is_staff,
|
isMasquerading: data.original_user_is_staff && !data.is_staff,
|
||||||
|
shortLinkFeatureFlag: data.mfe_short_url_is_active,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,6 +260,8 @@ function normalizeSequenceMetadata(sequence) {
|
|||||||
saveUnitPosition: sequence.save_position,
|
saveUnitPosition: sequence.save_position,
|
||||||
showCompletion: sequence.show_completion,
|
showCompletion: sequence.show_completion,
|
||||||
allowProctoringOptOut: sequence.allow_proctoring_opt_out,
|
allowProctoringOptOut: sequence.allow_proctoring_opt_out,
|
||||||
|
hash_key: sequence.hash_key,
|
||||||
|
decoded_id: sequence.decoded_id,
|
||||||
},
|
},
|
||||||
units: sequence.items.map(unit => ({
|
units: sequence.items.map(unit => ({
|
||||||
id: unit.id,
|
id: unit.id,
|
||||||
@@ -268,14 +272,14 @@ function normalizeSequenceMetadata(sequence) {
|
|||||||
contentType: unit.type,
|
contentType: unit.type,
|
||||||
graded: unit.graded,
|
graded: unit.graded,
|
||||||
containsContentTypeGatedContent: unit.contains_content_type_gated_content,
|
containsContentTypeGatedContent: unit.contains_content_type_gated_content,
|
||||||
|
decoded_id: unit.decoded_id,
|
||||||
|
hash_key: unit.hash_key,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSequenceMetadata(sequenceId) {
|
export async function getSequenceMetadata(sequenceId) {
|
||||||
const { data } = await getAuthenticatedHttpClient()
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
.get(`${getConfig().LMS_BASE_URL}/api/courseware/sequence/${sequenceId}`, {});
|
.get(`${getConfig().LMS_BASE_URL}/api/courseware/sequence/${sequenceId}`, {});
|
||||||
|
|
||||||
return normalizeSequenceMetadata(data);
|
return normalizeSequenceMetadata(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -254,8 +254,7 @@ describe('Data layer integration tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Test checkBlockCompletion', () => {
|
describe('Test checkBlockCompletion', () => {
|
||||||
const getCompletionURL = `${getConfig().LMS_BASE_URL}/courses/${courseId}/xblock/${sequenceId}/handler/get_completion`;
|
const getCompletionURL = `${getConfig().LMS_BASE_URL}/courses/${courseId}/xblock/${sequenceMetadata.decoded_id}/handler/get_completion`;
|
||||||
|
|
||||||
it('Should fail to check completion and log error', async () => {
|
it('Should fail to check completion and log error', async () => {
|
||||||
axiosMock.onPost(getCompletionURL).networkError();
|
axiosMock.onPost(getCompletionURL).networkError();
|
||||||
|
|
||||||
@@ -283,7 +282,7 @@ describe('Data layer integration tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Test saveSequencePosition', () => {
|
describe('Test saveSequencePosition', () => {
|
||||||
const gotoPositionURL = `${getConfig().LMS_BASE_URL}/courses/${courseId}/xblock/${sequenceId}/handler/goto_position`;
|
const gotoPositionURL = `${getConfig().LMS_BASE_URL}/courses/${courseId}/xblock/${sequenceMetadata.decoded_id}/handler/goto_position`;
|
||||||
|
|
||||||
it('Should change and revert sequence model activeUnitIndex in case of error', async () => {
|
it('Should change and revert sequence model activeUnitIndex in case of error', async () => {
|
||||||
axiosMock.onPost(gotoPositionURL).networkError();
|
axiosMock.onPost(gotoPositionURL).networkError();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const slice = createSlice({
|
|||||||
sequenceId: null,
|
sequenceId: null,
|
||||||
specialExamsEnabledWaffleFlag: false,
|
specialExamsEnabledWaffleFlag: false,
|
||||||
proctoredExamsEnabledWaffleFlag: false,
|
proctoredExamsEnabledWaffleFlag: false,
|
||||||
|
shortLinkFeatureFlag: false,
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
setsSpecialExamsEnabled: (state, { payload }) => {
|
setsSpecialExamsEnabled: (state, { payload }) => {
|
||||||
@@ -23,6 +24,9 @@ const slice = createSlice({
|
|||||||
setsProctoredExamsEnabled: (state, { payload }) => {
|
setsProctoredExamsEnabled: (state, { payload }) => {
|
||||||
state.proctoredExamsEnabledWaffleFlag = payload.proctoredExamsEnabledWaffleFlag;
|
state.proctoredExamsEnabledWaffleFlag = payload.proctoredExamsEnabledWaffleFlag;
|
||||||
},
|
},
|
||||||
|
setsShortLinkFeatureFlag: (state, { payload }) => {
|
||||||
|
state.shortLinkFeatureFlag = payload.shortLinkFeatureFlag;
|
||||||
|
},
|
||||||
fetchCourseRequest: (state, { payload }) => {
|
fetchCourseRequest: (state, { payload }) => {
|
||||||
state.courseId = payload.courseId;
|
state.courseId = payload.courseId;
|
||||||
state.courseStatus = LOADING;
|
state.courseStatus = LOADING;
|
||||||
@@ -57,6 +61,7 @@ const slice = createSlice({
|
|||||||
export const {
|
export const {
|
||||||
setsSpecialExamsEnabled,
|
setsSpecialExamsEnabled,
|
||||||
setsProctoredExamsEnabled,
|
setsProctoredExamsEnabled,
|
||||||
|
setsShortLinkFeatureFlag,
|
||||||
fetchCourseRequest,
|
fetchCourseRequest,
|
||||||
fetchCourseSuccess,
|
fetchCourseSuccess,
|
||||||
fetchCourseFailure,
|
fetchCourseFailure,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
setsSpecialExamsEnabled,
|
setsSpecialExamsEnabled,
|
||||||
setsProctoredExamsEnabled,
|
setsProctoredExamsEnabled,
|
||||||
|
setsShortLinkFeatureFlag,
|
||||||
fetchCourseRequest,
|
fetchCourseRequest,
|
||||||
fetchCourseSuccess,
|
fetchCourseSuccess,
|
||||||
fetchCourseFailure,
|
fetchCourseFailure,
|
||||||
@@ -100,6 +101,7 @@ function mergeLearningSequencesWithCourseBlocks(learningSequencesModels, courseB
|
|||||||
effortActivities: blocksSequence.effortActivities,
|
effortActivities: blocksSequence.effortActivities,
|
||||||
effortTime: blocksSequence.effortTime,
|
effortTime: blocksSequence.effortTime,
|
||||||
legacyWebUrl: blocksSequence.legacyWebUrl,
|
legacyWebUrl: blocksSequence.legacyWebUrl,
|
||||||
|
hash_key: blocksSequence.hash_key,
|
||||||
unitIds: blocksSequence.unitIds,
|
unitIds: blocksSequence.unitIds,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,6 +153,9 @@ export function fetchCourse(courseId) {
|
|||||||
dispatch(setsProctoredExamsEnabled({
|
dispatch(setsProctoredExamsEnabled({
|
||||||
proctoredExamsEnabledWaffleFlag: courseMetadataResult.value.proctoredExamsEnabledWaffleFlag,
|
proctoredExamsEnabledWaffleFlag: courseMetadataResult.value.proctoredExamsEnabledWaffleFlag,
|
||||||
}));
|
}));
|
||||||
|
dispatch(setsShortLinkFeatureFlag({
|
||||||
|
shortLinkFeatureFlag: courseMetadataResult.value.shortLinkFeatureFlag,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (courseBlocksResult.status === 'fulfilled') {
|
if (courseBlocksResult.status === 'fulfilled') {
|
||||||
@@ -176,10 +181,18 @@ export function fetchCourse(courseId) {
|
|||||||
modelType: 'sequences',
|
modelType: 'sequences',
|
||||||
modelsMap: sequences,
|
modelsMap: sequences,
|
||||||
}));
|
}));
|
||||||
|
dispatch(addModelsMap({
|
||||||
|
modelType: 'sequenceIdToHashKeyMap',
|
||||||
|
modelsMap: sequences,
|
||||||
|
}));
|
||||||
dispatch(updateModelsMap({
|
dispatch(updateModelsMap({
|
||||||
modelType: 'units',
|
modelType: 'units',
|
||||||
modelsMap: units,
|
modelsMap: units,
|
||||||
}));
|
}));
|
||||||
|
dispatch(addModelsMap({
|
||||||
|
modelType: 'unitIdToHashKeyMap',
|
||||||
|
modelsMap: units,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchedMetadata = courseMetadataResult.status === 'fulfilled';
|
const fetchedMetadata = courseMetadataResult.status === 'fulfilled';
|
||||||
@@ -231,10 +244,18 @@ export function fetchSequence(sequenceId) {
|
|||||||
modelType: 'sequences',
|
modelType: 'sequences',
|
||||||
model: sequence,
|
model: sequence,
|
||||||
}));
|
}));
|
||||||
|
dispatch(updateModel({
|
||||||
|
modelType: 'sequenceIdToHashKeyMap',
|
||||||
|
model: sequence,
|
||||||
|
}));
|
||||||
dispatch(updateModels({
|
dispatch(updateModels({
|
||||||
modelType: 'units',
|
modelType: 'units',
|
||||||
models: units,
|
models: units,
|
||||||
}));
|
}));
|
||||||
|
dispatch(updateModels({
|
||||||
|
modelType: 'unitIdToHashKeyMap',
|
||||||
|
models: units,
|
||||||
|
}));
|
||||||
dispatch(fetchSequenceSuccess({ sequenceId }));
|
dispatch(fetchSequenceSuccess({ sequenceId }));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -247,16 +268,24 @@ export function fetchSequence(sequenceId) {
|
|||||||
export function checkBlockCompletion(courseId, sequenceId, unitId) {
|
export function checkBlockCompletion(courseId, sequenceId, unitId) {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const { models } = getState();
|
const { models } = getState();
|
||||||
if (models.units[unitId].complete) {
|
let modelsUnitId = unitId;
|
||||||
|
let modelsSequenceId = sequenceId;
|
||||||
|
if (!models.units[unitId]) {
|
||||||
|
modelsUnitId = models.unitIdToHashKeyMap[unitId];
|
||||||
|
}
|
||||||
|
if (!models.sequences[sequenceId]) {
|
||||||
|
modelsSequenceId = models.sequenceIdToHashKeyMap[sequenceId];
|
||||||
|
}
|
||||||
|
if (models.units[modelsUnitId].complete) {
|
||||||
return; // do nothing. Things don't get uncompleted after they are completed.
|
return; // do nothing. Things don't get uncompleted after they are completed.
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isComplete = await getBlockCompletion(courseId, sequenceId, unitId);
|
const isComplete = await getBlockCompletion(courseId, modelsSequenceId, modelsUnitId);
|
||||||
dispatch(updateModel({
|
dispatch(updateModel({
|
||||||
modelType: 'units',
|
modelType: 'units',
|
||||||
model: {
|
model: {
|
||||||
id: unitId,
|
id: modelsUnitId,
|
||||||
complete: isComplete,
|
complete: isComplete,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -269,23 +298,27 @@ export function checkBlockCompletion(courseId, sequenceId, unitId) {
|
|||||||
export function saveSequencePosition(courseId, sequenceId, activeUnitIndex) {
|
export function saveSequencePosition(courseId, sequenceId, activeUnitIndex) {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const { models } = getState();
|
const { models } = getState();
|
||||||
const initialActiveUnitIndex = models.sequences[sequenceId].activeUnitIndex;
|
let modelsSequenceId = sequenceId;
|
||||||
|
if (!models.sequences[sequenceId]) {
|
||||||
|
modelsSequenceId = models.sequenceIdToHashKeyMap[sequenceId];
|
||||||
|
}
|
||||||
|
const initialActiveUnitIndex = models.sequences[modelsSequenceId].activeUnitIndex;
|
||||||
// Optimistically update the position.
|
// Optimistically update the position.
|
||||||
dispatch(updateModel({
|
dispatch(updateModel({
|
||||||
modelType: 'sequences',
|
modelType: 'sequences',
|
||||||
model: {
|
model: {
|
||||||
id: sequenceId,
|
id: modelsSequenceId,
|
||||||
activeUnitIndex,
|
activeUnitIndex,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
try {
|
try {
|
||||||
await postSequencePosition(courseId, sequenceId, activeUnitIndex);
|
await postSequencePosition(courseId, modelsSequenceId, activeUnitIndex);
|
||||||
// Update again under the assumption that the above call succeeded, since it doesn't return a
|
// Update again under the assumption that the above call succeeded, since it doesn't return a
|
||||||
// meaningful response.
|
// meaningful response.
|
||||||
dispatch(updateModel({
|
dispatch(updateModel({
|
||||||
modelType: 'sequences',
|
modelType: 'sequences',
|
||||||
model: {
|
model: {
|
||||||
id: sequenceId,
|
id: modelsSequenceId,
|
||||||
activeUnitIndex,
|
activeUnitIndex,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -294,7 +327,7 @@ export function saveSequencePosition(courseId, sequenceId, activeUnitIndex) {
|
|||||||
dispatch(updateModel({
|
dispatch(updateModel({
|
||||||
modelType: 'sequences',
|
modelType: 'sequences',
|
||||||
model: {
|
model: {
|
||||||
id: sequenceId,
|
id: modelsSequenceId,
|
||||||
activeUnitIndex: initialActiveUnitIndex,
|
activeUnitIndex: initialActiveUnitIndex,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ function add(state, modelType, model) {
|
|||||||
if (state[modelType] === undefined) {
|
if (state[modelType] === undefined) {
|
||||||
state[modelType] = {};
|
state[modelType] = {};
|
||||||
}
|
}
|
||||||
|
if (modelType.includes('IdToHashKeyMap')) {
|
||||||
|
state[modelType][model.hash_key] = id;
|
||||||
|
}
|
||||||
state[modelType][id] = model;
|
state[modelType][id] = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,6 +16,9 @@ function update(state, modelType, model) {
|
|||||||
if (state[modelType] === undefined) {
|
if (state[modelType] === undefined) {
|
||||||
state[modelType] = {};
|
state[modelType] = {};
|
||||||
}
|
}
|
||||||
|
if (modelType.includes('IdToHashKeyMap')) {
|
||||||
|
state[modelType][model.hash_key] = model.id;
|
||||||
|
}
|
||||||
state[modelType][model.id] = { ...state[modelType][model.id], ...model };
|
state[modelType][model.id] = { ...state[modelType][model.id], ...model };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,20 +34,21 @@ subscribe(APP_READY, () => {
|
|||||||
<UserMessagesProvider>
|
<UserMessagesProvider>
|
||||||
<Switch>
|
<Switch>
|
||||||
<PageRoute path="/redirect" component={CoursewareRedirectLandingPage} />
|
<PageRoute path="/redirect" component={CoursewareRedirectLandingPage} />
|
||||||
<PageRoute path="/course/:courseId/home">
|
<PageRoute path="/course" component={CoursewareRedirectLandingPage} />
|
||||||
|
<PageRoute path="/c/:courseId/home">
|
||||||
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
|
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
|
||||||
<OutlineTab />
|
<OutlineTab />
|
||||||
</TabContainer>
|
</TabContainer>
|
||||||
</PageRoute>
|
</PageRoute>
|
||||||
<PageRoute path="/course/:courseId/dates">
|
<PageRoute path="/c/:courseId/dates">
|
||||||
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
|
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
|
||||||
<DatesTab />
|
<DatesTab />
|
||||||
</TabContainer>
|
</TabContainer>
|
||||||
</PageRoute>
|
</PageRoute>
|
||||||
<PageRoute
|
<PageRoute
|
||||||
path={[
|
path={[
|
||||||
'/course/:courseId/progress/:targetUserId/',
|
'/c/:courseId/progress/:targetUserId/',
|
||||||
'/course/:courseId/progress',
|
'/c/:courseId/progress',
|
||||||
]}
|
]}
|
||||||
render={({ match }) => (
|
render={({ match }) => (
|
||||||
<TabContainer
|
<TabContainer
|
||||||
@@ -59,16 +60,16 @@ subscribe(APP_READY, () => {
|
|||||||
</TabContainer>
|
</TabContainer>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<PageRoute path="/course/:courseId/course-end">
|
<PageRoute path="/c/:courseId/course-end">
|
||||||
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
|
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
|
||||||
<CourseExit />
|
<CourseExit />
|
||||||
</TabContainer>
|
</TabContainer>
|
||||||
</PageRoute>
|
</PageRoute>
|
||||||
<PageRoute
|
<PageRoute
|
||||||
path={[
|
path={[
|
||||||
'/course/:courseId/:sequenceId/:unitId',
|
'/c/:courseId/:sequenceId/:unitId',
|
||||||
'/course/:courseId/:sequenceId',
|
'/c/:courseId/:sequenceId',
|
||||||
'/course/:courseId',
|
'/c/:courseId',
|
||||||
]}
|
]}
|
||||||
component={CoursewareContainer}
|
component={CoursewareContainer}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ Factory.define('block')
|
|||||||
|
|
||||||
return blockId;
|
return blockId;
|
||||||
})
|
})
|
||||||
|
.attr(
|
||||||
|
'hash_key', ['block_id'],
|
||||||
|
(blockId) => {
|
||||||
|
const len = blockId.length;
|
||||||
|
return blockId.substring(23, len);
|
||||||
|
},
|
||||||
|
)
|
||||||
.attr(
|
.attr(
|
||||||
'id',
|
'id',
|
||||||
['id', 'block_id', 'type', 'courseId'],
|
['id', 'block_id', 'type', 'courseId'],
|
||||||
@@ -35,25 +42,33 @@ Factory.define('block')
|
|||||||
return `block-v1:${courseInfo}+type@${type}+block@${blockId}`;
|
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(
|
.attr(
|
||||||
'student_view_url',
|
'student_view_url',
|
||||||
['student_view_url', 'host', 'id'],
|
['student_view_url', 'host', 'decoded_id'],
|
||||||
(url, host, id) => {
|
(url, host, decodedId) => {
|
||||||
if (url) {
|
if (url) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${host}/xblock/${id}`;
|
return `${host}/xblock/${decodedId}`;
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.attr(
|
.attr(
|
||||||
'legacy_web_url',
|
'legacy_web_url',
|
||||||
['legacy_web_url', 'host', 'courseId', 'id'],
|
['legacy_web_url', 'host', 'courseId', 'decoded_id'],
|
||||||
(url, host, courseId, id) => {
|
(url, host, courseId, decodedId) => {
|
||||||
if (url) {
|
if (url) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${host}/courses/${courseId}/jump_to/${id}?experience=legacy`;
|
return `${host}/courses/${courseId}/jump_to/${decodedId}?experience=legacy`;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ describe('Tab Container', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
||||||
history.push(`/course/${courseId}`);
|
history.push(`/c/${courseId}`);
|
||||||
render(
|
render(
|
||||||
<Route path="/course/:courseId">
|
<Route path="/c/:courseId">
|
||||||
<TabContainer {...mockData} />
|
<TabContainer {...mockData} />
|
||||||
</Route>,
|
</Route>,
|
||||||
);
|
);
|
||||||
@@ -48,11 +48,11 @@ describe('Tab Container', () => {
|
|||||||
|
|
||||||
it('Should handle passing in a targetUserId', () => {
|
it('Should handle passing in a targetUserId', () => {
|
||||||
const targetUserId = '1';
|
const targetUserId = '1';
|
||||||
history.push(`/course/${courseId}/progress/${targetUserId}/`);
|
history.push(`/c/${courseId}/progress/${targetUserId}/`);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<Route
|
<Route
|
||||||
path="/course/:courseId/progress/:targetUserId/"
|
path="/c/:courseId/progress/:targetUserId/"
|
||||||
render={({ match }) => (
|
render={({ match }) => (
|
||||||
<TabContainer
|
<TabContainer
|
||||||
fetch={() => mockFetch(match.params.courseId, match.params.targetUserId)}
|
fetch={() => mockFetch(match.params.courseId, match.params.targetUserId)}
|
||||||
|
|||||||
Reference in New Issue
Block a user