From 04c14274fda2fa9cbe843e8bf02a9d8d560d9f8e Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 6 Dec 2023 20:36:29 +0530 Subject: [PATCH] feat: course outline page (#694) * feat: Course outline Top level page (#36) * feat: [2u-259] add components * feat: [2u-259] fix sidebar * feat: [2u-259] add tests, fix links * feat: [2u-259] fix messages * feat: [2u-159] fix reducer and sidebar * feat: [2u-259] fix reducer * feat: [2u-259] remove warning from selectors * feat: [2u-259] remove indents --------- Co-authored-by: Vladislav Keblysh feat: Course outline Status Bar (#50) * feat: [2u-259] add components * feat: [2u-259] fix sidebar * feat: [2u-259] add tests, fix links * feat: [2u-259] fix messages * feat: [2u-159] fix reducer and sidebar * feat: add checklist * feat: [2u-259] fix reducer * feat: [2u-259] remove warning from selectors * feat: [2u-259] remove indents * feat: [2u-259] add api, enable modal * feat: [2u-259] add tests * feat: [2u-259] add translates * feat: [2u-271] fix transalates * feat: [2u-281] fix isQuery pending, utils, hooks * feat: [2u-281] fix useScrollToHashElement * feat: [2u-271] fix imports --------- Co-authored-by: Vladislav Keblysh feat: Course Outline Reindex (#55) * feat: [2u-277] add alerts * feat: [2u-277] add translates * feat: [2u-277] fix tests * fix: [2u-277] fix slice and hook --------- Co-authored-by: Vladislav Keblysh fix: Course outline tests (#56) * fix: fixed course outline status bar tests * fix: fixed course outline status bar tests * fix: fixed course outline enable highlights modal tests * fix: enable modal tests fix: increase code coverage on the page * refactor: improve course outline page feat: lms live link chore: update outline link fix: course outline link refactor: remove unnecessary css and rename test file refactor: remove unnecessary css from outlineSidebar test: make use of message variable instead of hardcoded text refactor: remove unnecessary h5 class test: use test id for detecting component refactor: update course outline url and some default messages --------- Co-authored-by: vladislavkeblysh <138868841+vladislavkeblysh@users.noreply.github.com> --- .env.development | 1 - src/CourseAuthoringRoutes.jsx | 5 +- src/course-outline/CourseOutline.jsx | 141 + src/course-outline/CourseOutline.scss | 2 + src/course-outline/CourseOutline.test.jsx | 150 + .../__mocks__/courseBestPractices.js | 43 + src/course-outline/__mocks__/courseLaunch.js | 31 + .../__mocks__/courseOutlineIndex.js | 2952 +++++++++++++++++ .../courseOutlineIndexWithoutSections.js | 25 + src/course-outline/__mocks__/index.js | 4 + src/course-outline/constants.js | 59 + src/course-outline/data/api.js | 107 + src/course-outline/data/selectors.js | 4 + src/course-outline/data/slice.js | 77 + src/course-outline/data/thunk.js | 104 + .../EnableHighlightsModal.jsx | 59 + .../EnableHighlightsModal.test.jsx | 55 + .../enable-highlights-modal/messages.js | 30 + .../header-navigations/HeaderNavigations.jsx | 100 + .../header-navigations/HeaderNavigations.scss | 4 + .../HeaderNavigations.test.jsx | 81 + .../header-navigations/messages.js | 38 + src/course-outline/hooks.jsx | 99 + src/course-outline/index.js | 2 + src/course-outline/messages.js | 34 + .../outline-sidebar/OutlineSidebar.jsx | 66 + .../outline-sidebar/OutlineSidebar.test.jsx | 84 + .../outline-sidebar/messages.js | 70 + src/course-outline/outline-sidebar/utils.jsx | 69 + src/course-outline/status-bar/StatusBar.jsx | 116 + src/course-outline/status-bar/StatusBar.scss | 12 + .../status-bar/StatusBar.test.jsx | 112 + src/course-outline/status-bar/messages.js | 46 + .../utils/courseChecklistValidators.js | 106 + .../utils/courseChecklistValidators.test.js | 297 ++ .../utils/getChecklistForStatusBar.js | 79 + .../utils/getChecklistValues.js | 79 + .../utils/getChecklistValues.test.js | 86 + src/help-urls/__mocks__/helpUrls.js | 35 + src/help-urls/__mocks__/index.js | 2 + src/help-urls/data/api.js | 4 +- src/hooks.js | 18 + src/i18n/messages/ar.json | 2 +- src/i18n/messages/de.json | 2 +- src/i18n/messages/de_DE.json | 2 +- src/i18n/messages/es_419.json | 2 +- src/i18n/messages/fr.json | 2 +- src/i18n/messages/fr_CA.json | 2 +- src/i18n/messages/hi.json | 2 +- src/i18n/messages/it.json | 2 +- src/i18n/messages/it_IT.json | 2 +- src/i18n/messages/pt.json | 2 +- src/i18n/messages/pt_PT.json | 2 +- src/i18n/messages/ru.json | 2 +- src/i18n/messages/uk.json | 2 +- src/i18n/messages/zh_CN.json | 2 +- src/index.scss | 1 + src/schedule-and-details/index.jsx | 5 +- .../entrance-exam/index.jsx | 2 +- .../schedule-section/index.jsx | 2 +- src/store.js | 2 + 61 files changed, 5507 insertions(+), 21 deletions(-) create mode 100644 src/course-outline/CourseOutline.jsx create mode 100644 src/course-outline/CourseOutline.scss create mode 100644 src/course-outline/CourseOutline.test.jsx create mode 100644 src/course-outline/__mocks__/courseBestPractices.js create mode 100644 src/course-outline/__mocks__/courseLaunch.js create mode 100644 src/course-outline/__mocks__/courseOutlineIndex.js create mode 100644 src/course-outline/__mocks__/courseOutlineIndexWithoutSections.js create mode 100644 src/course-outline/__mocks__/index.js create mode 100644 src/course-outline/constants.js create mode 100644 src/course-outline/data/api.js create mode 100644 src/course-outline/data/selectors.js create mode 100644 src/course-outline/data/slice.js create mode 100644 src/course-outline/data/thunk.js create mode 100644 src/course-outline/enable-highlights-modal/EnableHighlightsModal.jsx create mode 100644 src/course-outline/enable-highlights-modal/EnableHighlightsModal.test.jsx create mode 100644 src/course-outline/enable-highlights-modal/messages.js create mode 100644 src/course-outline/header-navigations/HeaderNavigations.jsx create mode 100644 src/course-outline/header-navigations/HeaderNavigations.scss create mode 100644 src/course-outline/header-navigations/HeaderNavigations.test.jsx create mode 100644 src/course-outline/header-navigations/messages.js create mode 100644 src/course-outline/hooks.jsx create mode 100644 src/course-outline/index.js create mode 100644 src/course-outline/messages.js create mode 100644 src/course-outline/outline-sidebar/OutlineSidebar.jsx create mode 100644 src/course-outline/outline-sidebar/OutlineSidebar.test.jsx create mode 100644 src/course-outline/outline-sidebar/messages.js create mode 100644 src/course-outline/outline-sidebar/utils.jsx create mode 100644 src/course-outline/status-bar/StatusBar.jsx create mode 100644 src/course-outline/status-bar/StatusBar.scss create mode 100644 src/course-outline/status-bar/StatusBar.test.jsx create mode 100644 src/course-outline/status-bar/messages.js create mode 100644 src/course-outline/utils/courseChecklistValidators.js create mode 100644 src/course-outline/utils/courseChecklistValidators.test.js create mode 100644 src/course-outline/utils/getChecklistForStatusBar.js create mode 100644 src/course-outline/utils/getChecklistValues.js create mode 100644 src/course-outline/utils/getChecklistValues.test.js create mode 100644 src/help-urls/__mocks__/helpUrls.js create mode 100644 src/help-urls/__mocks__/index.js create mode 100644 src/hooks.js diff --git a/.env.development b/.env.development index cc50a024f..55b8ce70c 100644 --- a/.env.development +++ b/.env.development @@ -33,7 +33,6 @@ ENABLE_ACCESSIBILITY_PAGE=false ENABLE_PROGRESS_GRAPH_SETTINGS=false ENABLE_TEAM_TYPE_SETTING=false ENABLE_NEW_EDITOR_PAGES=true -ENABLE_NEW_COURSE_OUTLINE_PAGE = false ENABLE_NEW_VIDEO_UPLOAD_PAGE = false ENABLE_UNIT_PAGE = false ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = false diff --git a/src/CourseAuthoringRoutes.jsx b/src/CourseAuthoringRoutes.jsx index 0aa1043a6..6ff7f475b 100644 --- a/src/CourseAuthoringRoutes.jsx +++ b/src/CourseAuthoringRoutes.jsx @@ -11,6 +11,7 @@ import VideoSelectorContainer from './selectors/VideoSelectorContainer'; import CustomPages from './custom-pages'; import { FilesPage, VideosPage } from './files-and-videos'; import { AdvancedSettings } from './advanced-settings'; +import { CourseOutline } from './course-outline'; import ScheduleAndDetails from './schedule-and-details'; import { GradingSettings } from './grading-settings'; import CourseTeam from './course-team/CourseTeam'; @@ -41,8 +42,8 @@ const CourseAuthoringRoutes = () => { : null} + path="/" + element={} /> { + const intl = useIntl(); + + const { + savingStatus, + statusBarData, + isLoading, + isReIndexShow, + showErrorAlert, + showSuccessAlert, + isSectionsExpanded, + isEnableHighlightsModalOpen, + isInternetConnectionAlertFailed, + isDisabledReindexButton, + headerNavigationsActions, + openEnableHighlightsModal, + closeEnableHighlightsModal, + handleEnableHighlightsSubmit, + handleInternetConnectionFailed, + } = useCourseOutline({ courseId }); + + if (isLoading) { + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>; + } + + return ( + <> + +
+ + {showSuccessAlert ? ( + + + )} + /> + + +
+
+
+ +
+
+
+
+ + + +
+ +
+
+
+ + {showErrorAlert && ( +
+ + ); +}; + +CourseOutline.propTypes = { + courseId: PropTypes.string.isRequired, +}; + +export default CourseOutline; diff --git a/src/course-outline/CourseOutline.scss b/src/course-outline/CourseOutline.scss new file mode 100644 index 000000000..732420365 --- /dev/null +++ b/src/course-outline/CourseOutline.scss @@ -0,0 +1,2 @@ +@import "./header-navigations/HeaderNavigations"; +@import "./status-bar/StatusBar"; diff --git a/src/course-outline/CourseOutline.test.jsx b/src/course-outline/CourseOutline.test.jsx new file mode 100644 index 000000000..b6d98bc47 --- /dev/null +++ b/src/course-outline/CourseOutline.test.jsx @@ -0,0 +1,150 @@ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { initializeMockApp } from '@edx/frontend-platform'; +import MockAdapter from 'axios-mock-adapter'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +import { + getCourseBestPracticesApiUrl, + getCourseLaunchApiUrl, + getCourseOutlineIndexApiUrl, + getCourseReindexApiUrl, + getEnableHighlightsEmailsApiUrl, +} from './data/api'; +import { + enableCourseHighlightsEmailsQuery, + fetchCourseBestPracticesQuery, + fetchCourseLaunchQuery, + fetchCourseOutlineIndexQuery, + fetchCourseReindexQuery, +} from './data/thunk'; +import initializeStore from '../store'; +import { + courseOutlineIndexMock, + courseBestPracticesMock, + courseLaunchMock, +} from './__mocks__'; +import { executeThunk } from '../utils'; +import CourseOutline from './CourseOutline'; +import messages from './messages'; + +let axiosMock; +let store; +const mockPathname = '/foo-bar'; +const courseId = '123'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: () => ({ + pathname: mockPathname, + }), +})); + +const RootWrapper = () => ( + + + + + +); + +describe('', () => { + beforeEach(async () => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + + store = initializeStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock + .onGet(getCourseOutlineIndexApiUrl(courseId)) + .reply(200, courseOutlineIndexMock); + await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch); + }); + + it('render CourseOutline component correctly', async () => { + const { getByText } = render(); + + await waitFor(() => { + expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument(); + }); + }); + + it('check reindex and render success alert is correctly', async () => { + const { getByText } = render(); + + axiosMock + .onGet(getCourseReindexApiUrl(courseOutlineIndexMock.reindexLink)) + .reply(200); + await executeThunk(fetchCourseReindexQuery(courseId, courseOutlineIndexMock.reindexLink), store.dispatch); + + expect(getByText(messages.alertSuccessDescription.defaultMessage)).toBeInTheDocument(); + }); + + it('render error alert after failed reindex correctly', async () => { + const { getByText } = render(); + + axiosMock + .onGet(getCourseReindexApiUrl('some link')) + .reply(500); + await executeThunk(fetchCourseReindexQuery(courseId, 'some link'), store.dispatch); + + expect(getByText(messages.alertErrorTitle.defaultMessage)).toBeInTheDocument(); + }); + + it('render checklist value correctly', async () => { + const { getByText } = render(); + + axiosMock + .onGet(getCourseBestPracticesApiUrl({ + courseId, excludeGraded: true, all: true, + })) + .reply(200, courseBestPracticesMock); + + axiosMock + .onGet(getCourseLaunchApiUrl({ + courseId, gradedOnly: true, validateOras: true, all: true, + })) + .reply(200, courseLaunchMock); + + await executeThunk(fetchCourseLaunchQuery({ + courseId, gradedOnly: true, validateOras: true, all: true, + }), store.dispatch); + await executeThunk(fetchCourseBestPracticesQuery({ + courseId, excludeGraded: true, all: true, + }), store.dispatch); + + expect(getByText('4/9 completed')).toBeInTheDocument(); + }); + + it('check highlights are enabled after enable highlights query is successful', async () => { + const { findByTestId } = render(); + + axiosMock + .onGet(getCourseOutlineIndexApiUrl(courseId)) + .reply(200, { + ...courseOutlineIndexMock, + highlightsEnabledForMessaging: false, + }); + + axiosMock + .onPost(getEnableHighlightsEmailsApiUrl(courseId), { + publish: 'republish', + metadata: { + highlights_enabled_for_messaging: true, + }, + }) + .reply(200); + + await executeThunk(enableCourseHighlightsEmailsQuery(courseId), store.dispatch); + expect(await findByTestId('highlights-enabled-span')).toBeInTheDocument(); + }); +}); diff --git a/src/course-outline/__mocks__/courseBestPractices.js b/src/course-outline/__mocks__/courseBestPractices.js new file mode 100644 index 000000000..494c54d85 --- /dev/null +++ b/src/course-outline/__mocks__/courseBestPractices.js @@ -0,0 +1,43 @@ +module.exports = { + isSelfPaced: false, + sections: { + totalNumber: 6, + totalVisible: 4, + numberWithHighlights: 2, + highlightsActiveForCourse: true, + highlightsEnabled: true, + }, + subsections: { + totalVisible: 5, + numWithOneBlockType: 2, + numBlockTypes: { + min: 0, + max: 3, + mean: 1, + median: 1, + mode: 1, + }, + }, + units: { + totalVisible: 9, + numBlocks: { + min: 1, + max: 2, + mean: 2, + median: 2, + mode: 2, + }, + }, + videos: { + totalNumber: 7, + numMobileEncoded: 0, + numWithValId: 3, + durations: { + min: null, + max: null, + mean: null, + median: null, + mode: null, + }, + }, +}; diff --git a/src/course-outline/__mocks__/courseLaunch.js b/src/course-outline/__mocks__/courseLaunch.js new file mode 100644 index 000000000..40b629f46 --- /dev/null +++ b/src/course-outline/__mocks__/courseLaunch.js @@ -0,0 +1,31 @@ +module.exports = { + isSelfPaced: false, + dates: { + hasStartDate: true, + hasEndDate: false, + }, + assignments: { + totalNumber: 11, + totalVisible: 7, + assignmentsWithDatesBeforeStart: [], + assignmentsWithDatesAfterEnd: [], + assignmentsWithOraDatesBeforeStart: [], + assignmentsWithOraDatesAfterEnd: [], + }, + grades: { + hasGradingPolicy: true, + sumOfWeights: 1, + }, + certificates: { + isActivated: false, + hasCertificate: false, + isEnabled: true, + }, + updates: { + hasUpdate: true, + }, + proctoring: { + needsProctoringEscalationEmail: false, + hasProctoringEscalationEmail: false, + }, +}; diff --git a/src/course-outline/__mocks__/courseOutlineIndex.js b/src/course-outline/__mocks__/courseOutlineIndex.js new file mode 100644 index 000000000..b65784c9b --- /dev/null +++ b/src/course-outline/__mocks__/courseOutlineIndex.js @@ -0,0 +1,2952 @@ +module.exports = { + courseReleaseDate: 'Set Date', + courseStructure: { + id: 'block-v1:edX+DemoX+Demo_Course+type@course+block@course', + displayName: 'Demonstration Course', + category: 'course', + hasChildren: true, + unitLevelDiscussions: false, + editedOn: 'Aug 23, 2023 at 12:35 UTC', + published: true, + publishedOn: 'Aug 23, 2023 at 11:32 UTC', + studioUrl: '/course/course-v1:edX+DemoX+Demo_Course', + releasedToStudents: false, + releaseDate: 'Nov 09, 2023 at 22:00 UTC', + visibilityState: null, + hasExplicitStaffLock: false, + start: '2023-11-09T22:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + highlightsEnabledForMessaging: true, + highlightsEnabled: true, + highlightsPreviewOnly: false, + highlightsDocUrl: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages', + enableProctoredExams: false, + createZendeskTickets: true, + enableTimedExams: true, + childInfo: { + category: 'chapter', + displayName: 'Section', + children: [ + { + id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b', + displayName: 'Introduction 12', + category: 'chapter', + hasChildren: true, + editedOn: 'Aug 23, 2023 at 12:35 UTC', + published: true, + publishedOn: 'Aug 23, 2023 at 12:35 UTC', + studioUrl: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40d8a6192ade314473a78242dfeedfbf5b', + releasedToStudents: true, + releaseDate: 'Aug 10, 2023 at 22:00 UTC', + visibilityState: 'staff_only', + hasExplicitStaffLock: true, + start: '2023-08-10T22:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + highlights: [ + 'New Highlight 1', + 'New Highlight 4', + ], + highlightsEnabled: true, + highlightsPreviewOnly: false, + highlightsDocUrl: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages', + childInfo: { + category: 'sequential', + displayName: 'Subsection', + children: [ + { + id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction', + displayName: 'Demo Course Overview', + category: 'sequential', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%40edx_introduction', + releasedToStudents: true, + releaseDate: 'Jan 01, 1970 at 05:00 UTC', + visibilityState: 'staff_only', + hasExplicitStaffLock: false, + start: '1970-01-01T05:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + hideAfterDue: false, + isProctoredExam: false, + wasExamEverLinkedWithExternal: false, + onlineProctoringRules: '', + isPracticeExam: false, + isOnboardingExam: false, + isTimeLimited: false, + examReviewRules: '', + defaultTimeLimitMinutes: null, + proctoringExamConfigurationLink: null, + supportsOnboarding: false, + showReviewRules: true, + childInfo: { + category: 'vertical', + displayName: 'Unit', + children: [ + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc', + displayName: 'Introduction: Video and Sequences', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc', + releasedToStudents: true, + releaseDate: 'Jan 01, 1970 at 05:00 UTC', + visibilityState: 'staff_only', + hasExplicitStaffLock: false, + start: '1970-01-01T05:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: true, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + ], + }, + ancestorHasStaffLock: true, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + ], + }, + ancestorHasStaffLock: false, + staffOnlyMessage: true, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions', + displayName: 'Example Week 2: Get Interactive', + category: 'chapter', + hasChildren: true, + editedOn: 'Aug 16, 2023 at 11:52 UTC', + published: true, + publishedOn: 'Aug 16, 2023 at 11:52 UTC', + studioUrl: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40graded_interactions', + releasedToStudents: false, + releaseDate: 'Nov 09, 2023 at 22:00 UTC', + visibilityState: 'ready', + hasExplicitStaffLock: false, + start: '2023-11-09T22:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + highlights: [ + 'New', + ], + highlightsEnabled: true, + highlightsPreviewOnly: false, + highlightsDocUrl: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages', + childInfo: { + category: 'sequential', + displayName: 'Subsection', + children: [ + { + id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@simulations', + displayName: "Lesson 2 - Let's Get Interactive!", + category: 'sequential', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%40simulations', + releasedToStudents: true, + releaseDate: 'Jan 01, 1970 at 05:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '1970-01-01T05:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + hideAfterDue: false, + isProctoredExam: false, + wasExamEverLinkedWithExternal: false, + onlineProctoringRules: '', + isPracticeExam: false, + isOnboardingExam: false, + isTimeLimited: false, + examReviewRules: '', + defaultTimeLimitMinutes: null, + proctoringExamConfigurationLink: null, + supportsOnboarding: false, + showReviewRules: true, + childInfo: { + category: 'vertical', + displayName: 'Unit', + children: [ + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d0d804e8863c4a95a659c04d8a2b2bc0', + displayName: "Lesson 2 - Let's Get Interactive! ", + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@d0d804e8863c4a95a659c04d8a2b2bc0', + releasedToStudents: true, + releaseDate: 'Jan 01, 1970 at 05:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '1970-01-01T05:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_2dbb0072785e', + displayName: 'An Interactive Reference Table', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_2dbb0072785e', + releasedToStudents: true, + releaseDate: 'Jan 01, 1970 at 05:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '1970-01-01T05:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_98cf62510471', + displayName: 'Zooming Diagrams', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_98cf62510471', + releasedToStudents: true, + releaseDate: 'Jan 01, 1970 at 05:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '1970-01-01T05:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_d32bf9b2242c', + displayName: 'Electronic Sound Experiment', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_d32bf9b2242c', + releasedToStudents: true, + releaseDate: 'Jan 01, 1970 at 05:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '1970-01-01T05:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4e592689563243c484af947465eaef0d', + displayName: 'New Unit', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@4e592689563243c484af947465eaef0d', + releasedToStudents: true, + releaseDate: 'Jan 01, 1970 at 05:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '1970-01-01T05:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + ], + }, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations', + displayName: 'Homework - Labs and Demos', + category: 'sequential', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%40graded_simulations', + releasedToStudents: true, + releaseDate: 'Feb 05, 2013 at 00:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '2013-02-05T00:00:00Z', + graded: true, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: 'Homework', + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + hideAfterDue: false, + isProctoredExam: false, + wasExamEverLinkedWithExternal: false, + onlineProctoringRules: '', + isPracticeExam: false, + isOnboardingExam: false, + isTimeLimited: false, + examReviewRules: '', + defaultTimeLimitMinutes: null, + proctoringExamConfigurationLink: null, + supportsOnboarding: false, + showReviewRules: true, + childInfo: { + category: 'vertical', + displayName: 'Unit', + children: [ + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d6cee45205a449369d7ef8f159b22bdf', + displayName: 'Labs and Demos', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@d6cee45205a449369d7ef8f159b22bdf', + releasedToStudents: true, + releaseDate: 'Feb 05, 2013 at 00:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '2013-02-05T00:00:00Z', + graded: true, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_aae927868e55', + displayName: 'Code Grader', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_aae927868e55', + releasedToStudents: true, + releaseDate: 'Feb 05, 2013 at 00:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '2013-02-05T00:00:00Z', + graded: true, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_c037f3757df1', + displayName: 'Electric Circuit Simulator', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_c037f3757df1', + releasedToStudents: true, + releaseDate: 'Feb 05, 2013 at 00:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '2013-02-05T00:00:00Z', + graded: true, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_bc69a47c6fae', + displayName: 'Protein Creator', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_bc69a47c6fae', + releasedToStudents: true, + releaseDate: 'Feb 05, 2013 at 00:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '2013-02-05T00:00:00Z', + graded: true, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@8f89194410954e768bde1764985454a7', + displayName: 'Molecule Structures', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@8f89194410954e768bde1764985454a7', + releasedToStudents: true, + releaseDate: 'Feb 05, 2013 at 00:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '2013-02-05T00:00:00Z', + graded: true, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + ], + }, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e', + displayName: 'Homework - Essays', + category: 'sequential', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%40175e76c4951144a29d46211361266e0e', + releasedToStudents: false, + releaseDate: 'Nov 09, 2023 at 22:00 UTC', + visibilityState: 'ready', + hasExplicitStaffLock: false, + start: '2023-11-09T22:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + hideAfterDue: false, + isProctoredExam: false, + wasExamEverLinkedWithExternal: false, + onlineProctoringRules: '', + isPracticeExam: false, + isOnboardingExam: false, + isTimeLimited: false, + examReviewRules: '', + defaultTimeLimitMinutes: null, + proctoringExamConfigurationLink: null, + supportsOnboarding: false, + showReviewRules: true, + childInfo: { + category: 'vertical', + displayName: 'Unit', + children: [ + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@fb79dcbad35b466a8c6364f8ffee9050', + displayName: 'Peer Assessed Essays', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@fb79dcbad35b466a8c6364f8ffee9050', + releasedToStudents: false, + releaseDate: 'Nov 09, 2023 at 22:00 UTC', + visibilityState: 'ready', + hasExplicitStaffLock: false, + start: '2023-11-09T22:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + ], + }, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + ], + }, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@1414ffd5143b4b508f739b563ab468b7', + displayName: 'About Exams and Certificates', + category: 'chapter', + hasChildren: true, + editedOn: 'Aug 10, 2023 at 10:40 UTC', + published: true, + publishedOn: 'Aug 10, 2023 at 10:40 UTC', + studioUrl: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%401414ffd5143b4b508f739b563ab468b7', + releasedToStudents: false, + releaseDate: 'Jan 01, 2030 at 05:00 UTC', + visibilityState: 'needs_attention', + hasExplicitStaffLock: false, + start: '2030-01-01T05:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + highlights: [], + highlightsEnabled: true, + highlightsPreviewOnly: false, + highlightsDocUrl: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages', + childInfo: { + category: 'sequential', + displayName: 'Subsection', + children: [ + { + id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow', + displayName: 'edX Exams', + category: 'sequential', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%40workflow', + releasedToStudents: true, + releaseDate: 'Feb 05, 2013 at 00:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '2013-02-05T00:00:00Z', + graded: true, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: 'Exam', + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + hideAfterDue: false, + isProctoredExam: false, + wasExamEverLinkedWithExternal: false, + onlineProctoringRules: '', + isPracticeExam: false, + isOnboardingExam: false, + isTimeLimited: false, + examReviewRules: '', + defaultTimeLimitMinutes: null, + proctoringExamConfigurationLink: null, + supportsOnboarding: false, + showReviewRules: true, + childInfo: { + category: 'vertical', + displayName: 'Unit', + children: [ + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@934cc32c177d41b580c8413e561346b3', + displayName: 'EdX Exams', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@934cc32c177d41b580c8413e561346b3', + releasedToStudents: true, + releaseDate: 'Feb 05, 2013 at 00:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '2013-02-05T00:00:00Z', + graded: true, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_f04afeac0131', + displayName: 'Immediate Feedback', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_f04afeac0131', + releasedToStudents: true, + releaseDate: 'Feb 05, 2013 at 00:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '2013-02-05T00:00:00Z', + graded: true, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@b6662b497c094bcc9b870d8270c90c93', + displayName: 'Getting Answers', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@b6662b497c094bcc9b870d8270c90c93', + releasedToStudents: true, + releaseDate: 'Feb 05, 2013 at 00:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '2013-02-05T00:00:00Z', + graded: true, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@f91d8d31f7cf48ce990f8d8745ae4cfa', + displayName: 'Answering More Than Once', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@f91d8d31f7cf48ce990f8d8745ae4cfa', + releasedToStudents: true, + releaseDate: 'Feb 05, 2013 at 00:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '2013-02-05T00:00:00Z', + graded: true, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_ac391cde8a91', + displayName: 'Limited Checks', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_ac391cde8a91', + releasedToStudents: true, + releaseDate: 'Feb 05, 2013 at 00:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '2013-02-05T00:00:00Z', + graded: true, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_36e0beb03f0a', + displayName: 'Randomized Questions', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_36e0beb03f0a', + releasedToStudents: true, + releaseDate: 'Feb 05, 2013 at 00:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '2013-02-05T00:00:00Z', + graded: true, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@1b0e2c2c84884b95b1c99fb678cc964c', + displayName: 'Overall Grade Performance', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@1b0e2c2c84884b95b1c99fb678cc964c', + releasedToStudents: true, + releaseDate: 'Feb 05, 2013 at 00:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '2013-02-05T00:00:00Z', + graded: true, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff', + displayName: 'Passing a Course', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff', + releasedToStudents: true, + releaseDate: 'Feb 05, 2013 at 00:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '2013-02-05T00:00:00Z', + graded: true, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d6eaa391d2be41dea20b8b1bfbcb1c45', + displayName: 'Getting Your edX Certificate', + category: 'vertical', + hasChildren: true, + editedOn: 'Jul 07, 2023 at 11:14 UTC', + published: true, + publishedOn: 'Jul 07, 2023 at 11:14 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@d6eaa391d2be41dea20b8b1bfbcb1c45', + releasedToStudents: true, + releaseDate: 'Feb 05, 2013 at 00:00 UTC', + visibilityState: 'live', + hasExplicitStaffLock: false, + start: '2013-02-05T00:00:00Z', + graded: true, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + ], + }, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + ], + }, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + { + id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@46e11a7b395f45b9837df6c6ac609004', + displayName: 'Publish section', + category: 'chapter', + hasChildren: true, + editedOn: 'Aug 23, 2023 at 12:22 UTC', + published: true, + publishedOn: 'Aug 23, 2023 at 12:22 UTC', + studioUrl: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%4046e11a7b395f45b9837df6c6ac609004', + releasedToStudents: false, + releaseDate: 'Nov 09, 2023 at 22:00 UTC', + visibilityState: 'ready', + hasExplicitStaffLock: false, + start: '2023-11-09T22:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + highlights: [], + highlightsEnabled: true, + highlightsPreviewOnly: false, + highlightsDocUrl: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages', + childInfo: { + category: 'sequential', + displayName: 'Subsection', + children: [ + { + id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@1945e9656cbe4abe8f2020c67e9e1f61', + displayName: 'Subsection sub', + category: 'sequential', + hasChildren: true, + editedOn: 'Aug 23, 2023 at 11:32 UTC', + published: true, + publishedOn: 'Aug 23, 2023 at 11:33 UTC', + studioUrl: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%401945e9656cbe4abe8f2020c67e9e1f61', + releasedToStudents: false, + releaseDate: 'Nov 09, 2023 at 22:00 UTC', + visibilityState: 'ready', + hasExplicitStaffLock: false, + start: '2023-11-09T22:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + hideAfterDue: false, + isProctoredExam: false, + wasExamEverLinkedWithExternal: false, + onlineProctoringRules: '', + isPracticeExam: false, + isOnboardingExam: false, + isTimeLimited: false, + examReviewRules: '', + defaultTimeLimitMinutes: null, + proctoringExamConfigurationLink: null, + supportsOnboarding: false, + showReviewRules: true, + childInfo: { + category: 'vertical', + displayName: 'Unit', + children: [ + { + id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@b8149aa5af944aed8eebf9c7dc9f3d0b', + displayName: 'Unit', + category: 'vertical', + hasChildren: true, + editedOn: 'Aug 23, 2023 at 11:32 UTC', + published: true, + publishedOn: 'Aug 23, 2023 at 11:33 UTC', + studioUrl: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@b8149aa5af944aed8eebf9c7dc9f3d0b', + releasedToStudents: false, + releaseDate: 'Nov 09, 2023 at 22:00 UTC', + visibilityState: 'ready', + hasExplicitStaffLock: false, + start: '2023-11-09T22:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Exam', + ], + hasChanges: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + ], + }, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + ], + }, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + ], + }, + ancestorHasStaffLock: false, + staffOnlyMessage: false, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, + }, + deprecatedBlocksInfo: { + deprecatedEnabledBlockTypes: [], + blocks: [], + advanceSettingsUrl: '/settings/advanced/course-v1:edx+101+y76', + }, + discussionsIncontextFeedbackUrl: '', + discussionsIncontextLearnmoreUrl: '', + initialState: { + expandedLocators: [ + 'block-v1:edx+101+y76+type@chapter+block@03de0adc9d1c4cc097062d80eb04abf6', + 'block-v1:edx+101+y76+type@sequential+block@8a85e287e30a47e98d8c1f37f74a6a9d', + ], + locatorToShow: 'block-v1:edx+101+y76+type@chapter+block@03de0adc9d1c4cc097062d80eb04abf6', + }, + languageCode: 'en', + lmsLink: '//localhost:18000/courses/course-v1:edx+101+y76/jump_to/block-v1:edx+101+y76+type@course+block@course', + mfeProctoredExamSettingsUrl: '', + notificationDismissUrl: '/course_notifications/course-v1:edx+101+y76/2', + proctoringErrors: [], + reindexLink: '/course/course-v1:edx+101+y76/search_reindex', + rerunNotificationId: 2, +}; diff --git a/src/course-outline/__mocks__/courseOutlineIndexWithoutSections.js b/src/course-outline/__mocks__/courseOutlineIndexWithoutSections.js new file mode 100644 index 000000000..89a48492d --- /dev/null +++ b/src/course-outline/__mocks__/courseOutlineIndexWithoutSections.js @@ -0,0 +1,25 @@ +module.exports = { + courseReleaseDate: 'Set Date', + courseStructure: {}, + deprecatedBlocksInfo: { + deprecatedEnabledBlockTypes: [], + blocks: [], + advanceSettingsUrl: '/settings/advanced/course-v1:edx+101+y76', + }, + discussionsIncontextFeedbackUrl: '', + discussionsIncontextLearnmoreUrl: '', + initialState: { + expandedLocators: [ + 'block-v1:edx+101+y76+type@chapter+block@03de0adc9d1c4cc097062d80eb04abf6', + 'block-v1:edx+101+y76+type@sequential+block@8a85e287e30a47e98d8c1f37f74a6a9d', + ], + locatorToShow: 'block-v1:edx+101+y76+type@chapter+block@03de0adc9d1c4cc097062d80eb04abf6', + }, + languageCode: 'en', + lmsLink: '//localhost:18000/courses/course-v1:edx+101+y76/jump_to/block-v1:edx+101+y76+type@course+block@course', + mfeProctoredExamSettingsUrl: '', + notificationDismissUrl: '/course_notifications/course-v1:edx+101+y76/2', + proctoringErrors: [], + reindexLink: '/course/course-v1:edx+101+y76/search_reindex', + rerunNotificationId: 2, +}; diff --git a/src/course-outline/__mocks__/index.js b/src/course-outline/__mocks__/index.js new file mode 100644 index 000000000..e699605ef --- /dev/null +++ b/src/course-outline/__mocks__/index.js @@ -0,0 +1,4 @@ +export { default as courseOutlineIndexMock } from './courseOutlineIndex'; +export { default as courseOutlineIndexWithoutSections } from './courseOutlineIndexWithoutSections'; +export { default as courseBestPracticesMock } from './courseBestPractices'; +export { default as courseLaunchMock } from './courseLaunch'; diff --git a/src/course-outline/constants.js b/src/course-outline/constants.js new file mode 100644 index 000000000..cd4c58eeb --- /dev/null +++ b/src/course-outline/constants.js @@ -0,0 +1,59 @@ +export const CHECKLIST_FILTERS = { + ALL: 'ALL', + SELF_PACED: 'SELF_PACED', + INSTRUCTOR_PACED: 'INSTRUCTOR_PACED', +}; + +export const LAUNCH_CHECKLIST = { + data: [ + { + id: 'welcomeMessage', + pacingTypeFilter: CHECKLIST_FILTERS.ALL, + }, + { + id: 'gradingPolicy', + pacingTypeFilter: CHECKLIST_FILTERS.ALL, + }, + { + id: 'certificate', + pacingTypeFilter: CHECKLIST_FILTERS.ALL, + }, + { + id: 'courseDates', + pacingTypeFilter: CHECKLIST_FILTERS.ALL, + }, + { + id: 'assignmentDeadlines', + pacingTypeFilter: CHECKLIST_FILTERS.INSTRUCTOR_PACED, + }, + { + id: 'proctoringEmail', + pacingTypeFilter: CHECKLIST_FILTERS.ALL, + }, + ], +}; + +export const BEST_PRACTICES_CHECKLIST = { + data: [ + { + id: 'videoDuration', + pacingTypeFilter: CHECKLIST_FILTERS.ALL, + }, + { + id: 'mobileFriendlyVideo', + pacingTypeFilter: CHECKLIST_FILTERS.ALL, + }, + { + id: 'diverseSequences', + pacingTypeFilter: CHECKLIST_FILTERS.ALL, + }, + { + id: 'weeklyHighlights', + pacingTypeFilter: CHECKLIST_FILTERS.SELF_PACED, + }, + { + id: 'unitDepth', + pacingTypeFilter: CHECKLIST_FILTERS.ALL, + }, + ], +}; diff --git a/src/course-outline/data/api.js b/src/course-outline/data/api.js new file mode 100644 index 000000000..9b613bf81 --- /dev/null +++ b/src/course-outline/data/api.js @@ -0,0 +1,107 @@ +import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; + +export const getCourseOutlineIndexApiUrl = (courseId) => `${getApiBaseUrl()}/api/contentstore/v1/course_index/${courseId}`; + +export const getCourseBestPracticesApiUrl = ({ + courseId, + excludeGraded, + all, +}) => `${getApiBaseUrl()}/api/courses/v1/quality/${courseId}/?exclude_graded=${excludeGraded}&all=${all}`; + +export const getCourseLaunchApiUrl = ({ + courseId, + gradedOnly, + validateOras, + all, +}) => `${getApiBaseUrl()}/api/courses/v1/validation/${courseId}/?graded_only=${gradedOnly}&validate_oras=${validateOras}&all=${all}`; + +export const getEnableHighlightsEmailsApiUrl = (courseId) => { + const formattedCourseId = courseId.split('course-v1:')[1]; + return `${getApiBaseUrl()}/xblock/block-v1:${formattedCourseId}+type@course+block@course`; +}; + +export const getCourseReindexApiUrl = (reindexLink) => `${getApiBaseUrl()}${reindexLink}`; + +/** + * Get course outline index. + * @param {string} courseId + * @returns {Promise} + */ +export async function getCourseOutlineIndex(courseId) { + const { data } = await getAuthenticatedHttpClient() + .get(getCourseOutlineIndexApiUrl(courseId)); + + return camelCaseObject(data); +} + +/** + * Get course best practices. + * @param {string} courseId + * @param {boolean} excludeGraded + * @param {boolean} all + * @returns {Promise} + */ +export async function getCourseBestPractices({ + courseId, + excludeGraded, + all, +}) { + const { data } = await getAuthenticatedHttpClient() + .get(getCourseBestPracticesApiUrl({ courseId, excludeGraded, all })); + + return camelCaseObject(data); +} + +/** + * Get course launch. + * @param {string} courseId + * @param {boolean} gradedOnly + * @param {boolean} validateOras + * @param {boolean} all + * @returns {Promise} + */ +export async function getCourseLaunch({ + courseId, + gradedOnly, + validateOras, + all, +}) { + const { data } = await getAuthenticatedHttpClient() + .get(getCourseLaunchApiUrl({ + courseId, gradedOnly, validateOras, all, + })); + + return camelCaseObject(data); +} + +/** + * Enable course highlights emails + * @param {string} courseId + * @returns {Promise} + */ +export async function enableCourseHighlightsEmails(courseId) { + const { data } = await getAuthenticatedHttpClient() + .post(getEnableHighlightsEmailsApiUrl(courseId), { + publish: 'republish', + metadata: { + highlights_enabled_for_messaging: true, + }, + }); + + return data; +} + +/** + * Restart reindex course + * @param {string} reindexLink + * @returns {Promise} + */ +export async function restartIndexingOnCourse(reindexLink) { + const { data } = await getAuthenticatedHttpClient() + .get(getCourseReindexApiUrl(reindexLink)); + + return camelCaseObject(data); +} diff --git a/src/course-outline/data/selectors.js b/src/course-outline/data/selectors.js new file mode 100644 index 000000000..096723ce5 --- /dev/null +++ b/src/course-outline/data/selectors.js @@ -0,0 +1,4 @@ +export const getOutlineIndexData = (state) => state.courseOutline.outlineIndexData; +export const getLoadingStatus = (state) => state.courseOutline.loadingStatus; +export const getStatusBarData = (state) => state.courseOutline.statusBarData; +export const getSavingStatus = (state) => state.courseOutline.savingStatus; diff --git a/src/course-outline/data/slice.js b/src/course-outline/data/slice.js new file mode 100644 index 000000000..ea08b4702 --- /dev/null +++ b/src/course-outline/data/slice.js @@ -0,0 +1,77 @@ +/* eslint-disable no-param-reassign */ +import { createSlice } from '@reduxjs/toolkit'; + +import { RequestStatus } from '../../data/constants'; + +const slice = createSlice({ + name: 'courseOutline', + initialState: { + loadingStatus: { + outlineIndexLoadingStatus: RequestStatus.IN_PROGRESS, + reIndexLoadingStatus: RequestStatus.IN_PROGRESS, + }, + outlineIndexData: {}, + savingStatus: '', + statusBarData: { + courseReleaseDate: '', + highlightsEnabledForMessaging: false, + highlightsDocUrl: '', + isSelfPaced: false, + checklist: { + totalCourseLaunchChecks: 0, + completedCourseLaunchChecks: 0, + totalCourseBestPracticesChecks: 0, + completedCourseBestPracticesChecks: 0, + }, + }, + }, + reducers: { + fetchOutlineIndexSuccess: (state, { payload }) => { + state.outlineIndexData = payload; + }, + updateOutlineIndexLoadingStatus: (state, { payload }) => { + state.loadingStatus = { + ...state.loadingStatus, + outlineIndexLoadingStatus: payload.status, + }; + }, + updateReindexLoadingStatus: (state, { payload }) => { + state.loadingStatus = { + ...state.loadingStatus, + reIndexLoadingStatus: payload.status, + }; + }, + updateStatusBar: (state, { payload }) => { + state.statusBarData = { + ...state.statusBarData, + ...payload, + }; + }, + fetchStatusBarChecklistSuccess: (state, { payload }) => { + state.statusBarData.checklist = { + ...state.statusBarData.checklist, + ...payload, + }; + }, + fetchStatusBarSelPacedSuccess: (state, { payload }) => { + state.statusBarData.isSelfPaced = payload.isSelfPaced; + }, + updateSavingStatus: (state, { payload }) => { + state.savingStatus = payload.status; + }, + }, +}); + +export const { + fetchOutlineIndexSuccess, + updateOutlineIndexLoadingStatus, + updateReindexLoadingStatus, + updateStatusBar, + fetchStatusBarChecklistSuccess, + fetchStatusBarSelPacedSuccess, + updateSavingStatus, +} = slice.actions; + +export const { + reducer, +} = slice; diff --git a/src/course-outline/data/thunk.js b/src/course-outline/data/thunk.js new file mode 100644 index 000000000..194ae03d7 --- /dev/null +++ b/src/course-outline/data/thunk.js @@ -0,0 +1,104 @@ +import { RequestStatus } from '../../data/constants'; +import { + getCourseBestPracticesChecklist, + getCourseLaunchChecklist, +} from '../utils/getChecklistForStatusBar'; +import { + enableCourseHighlightsEmails, + getCourseBestPractices, + getCourseLaunch, + getCourseOutlineIndex, + restartIndexingOnCourse, +} from './api'; +import { + fetchOutlineIndexSuccess, + updateOutlineIndexLoadingStatus, + updateReindexLoadingStatus, + updateStatusBar, + fetchStatusBarChecklistSuccess, + fetchStatusBarSelPacedSuccess, + updateSavingStatus, +} from './slice'; + +export function fetchCourseOutlineIndexQuery(courseId) { + return async (dispatch) => { + dispatch(updateOutlineIndexLoadingStatus({ status: RequestStatus.IN_PROGRESS })); + + try { + const outlineIndex = await getCourseOutlineIndex(courseId); + const { courseReleaseDate, courseStructure: { highlightsEnabledForMessaging, highlightsDocUrl } } = outlineIndex; + dispatch(fetchOutlineIndexSuccess(outlineIndex)); + dispatch(updateStatusBar({ courseReleaseDate, highlightsEnabledForMessaging, highlightsDocUrl })); + + dispatch(updateOutlineIndexLoadingStatus({ status: RequestStatus.SUCCESSFUL })); + } catch (error) { + dispatch(updateOutlineIndexLoadingStatus({ status: RequestStatus.FAILED })); + } + }; +} + +export function fetchCourseLaunchQuery({ + courseId, + gradedOnly = true, + validateOras = true, + all = true, +}) { + return async (dispatch) => { + try { + const data = await getCourseLaunch({ + courseId, gradedOnly, validateOras, all, + }); + dispatch(fetchStatusBarSelPacedSuccess({ isSelfPaced: data.isSelfPaced })); + dispatch(fetchStatusBarChecklistSuccess(getCourseLaunchChecklist(data))); + + return true; + } catch (error) { + return false; + } + }; +} + +export function fetchCourseBestPracticesQuery({ + courseId, + excludeGraded = true, + all = true, +}) { + return async (dispatch) => { + try { + const data = await getCourseBestPractices({ courseId, excludeGraded, all }); + dispatch(fetchStatusBarChecklistSuccess(getCourseBestPracticesChecklist(data))); + + return true; + } catch (error) { + return false; + } + }; +} + +export function enableCourseHighlightsEmailsQuery(courseId) { + return async (dispatch) => { + dispatch(updateSavingStatus({ status: RequestStatus.PENDING })); + + try { + await enableCourseHighlightsEmails(courseId); + dispatch(fetchCourseOutlineIndexQuery(courseId)); + + dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); + } catch (error) { + dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + } + }; +} + +export function fetchCourseReindexQuery(courseId, reindexLink) { + return async (dispatch) => { + dispatch(updateReindexLoadingStatus({ status: RequestStatus.IN_PROGRESS })); + + try { + await restartIndexingOnCourse(reindexLink); + dispatch(updateReindexLoadingStatus({ status: RequestStatus.SUCCESSFUL })); + } catch (error) { + dispatch(updateReindexLoadingStatus({ status: RequestStatus.FAILED })); + } + }; +} diff --git a/src/course-outline/enable-highlights-modal/EnableHighlightsModal.jsx b/src/course-outline/enable-highlights-modal/EnableHighlightsModal.jsx new file mode 100644 index 000000000..c7e4258aa --- /dev/null +++ b/src/course-outline/enable-highlights-modal/EnableHighlightsModal.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { + ActionRow, AlertModal, Button, Hyperlink, +} from '@edx/paragon'; + +import messages from './messages'; + +const EnableHighlightsModal = ({ + onEnableHighlightsSubmit, + isOpen, + close, + highlightsDocUrl, +}) => { + const intl = useIntl(); + + return ( + + + + + )} + > +

{intl.formatMessage(messages.description_1)}

+

+ {intl.formatMessage(messages.description_2)} + + {intl.formatMessage(messages.link)} + +

+
+ ); +}; + +EnableHighlightsModal.propTypes = { + onEnableHighlightsSubmit: PropTypes.func.isRequired, + isOpen: PropTypes.bool.isRequired, + close: PropTypes.func.isRequired, + highlightsDocUrl: PropTypes.string.isRequired, +}; + +export default EnableHighlightsModal; diff --git a/src/course-outline/enable-highlights-modal/EnableHighlightsModal.test.jsx b/src/course-outline/enable-highlights-modal/EnableHighlightsModal.test.jsx new file mode 100644 index 000000000..833631d03 --- /dev/null +++ b/src/course-outline/enable-highlights-modal/EnableHighlightsModal.test.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import EnableHighlightsModal from './EnableHighlightsModal'; +import messages from './messages'; + +const onEnableHighlightsSubmitMock = jest.fn(); +const closeMock = jest.fn(); + +const highlightsDocUrl = 'https://example.com/'; + +const renderComponent = (props) => render( + + + , +); + +describe('', () => { + it('renders EnableHighlightsModal component correctly', () => { + const { getByText, getByRole } = renderComponent(); + + expect(getByText(messages.title.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.description_1.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.description_2.defaultMessage)).toBeInTheDocument(); + expect(getByRole('button', { name: messages.cancelButton.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: messages.submitButton.defaultMessage })).toBeInTheDocument(); + + const hyperlink = getByText(messages.link.defaultMessage); + expect(hyperlink).toBeInTheDocument(); + expect(hyperlink.href).toBe(highlightsDocUrl); + }); + + it('calls onEnableHighlightsSubmit function when the "Submit" button is clicked', () => { + const { getByRole } = renderComponent(); + + const submitButton = getByRole('button', { name: messages.submitButton.defaultMessage }); + fireEvent.click(submitButton); + expect(onEnableHighlightsSubmitMock).toHaveBeenCalled(); + }); + + it('calls the close function when the "Cancel" button is clicked', () => { + const { getByRole } = renderComponent(); + + const cancelButton = getByRole('button', { name: messages.cancelButton.defaultMessage }); + fireEvent.click(cancelButton); + expect(closeMock).toHaveBeenCalled(); + }); +}); diff --git a/src/course-outline/enable-highlights-modal/messages.js b/src/course-outline/enable-highlights-modal/messages.js new file mode 100644 index 000000000..dbad07e94 --- /dev/null +++ b/src/course-outline/enable-highlights-modal/messages.js @@ -0,0 +1,30 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + title: { + id: 'course-authoring.course-outline.status-bar.modal.title', + defaultMessage: 'Enable course highlight emails', + }, + description_1: { + id: 'course-authoring.course-outline.status-bar.modal.description-1', + defaultMessage: 'When you enable course highlight emails, learners automatically receive email messages for each section that has highlights. You cannot disable highlights after you start sending them.', + }, + description_2: { + id: 'course-authoring.course-outline.status-bar.modal.description-2', + defaultMessage: 'Are you sure you want to enable course highlight emails?', + }, + link: { + id: 'course-authoring.course-outline.status-bar.modal.link', + defaultMessage: 'Learn more', + }, + cancelButton: { + id: 'course-authoring.course-outline.status-bar.modal.cancelButton', + defaultMessage: 'Cancel', + }, + submitButton: { + id: 'course-authoring.course-outline.status-bar.modal.submitButton', + defaultMessage: 'Enable', + }, +}); + +export default messages; diff --git a/src/course-outline/header-navigations/HeaderNavigations.jsx b/src/course-outline/header-navigations/HeaderNavigations.jsx new file mode 100644 index 000000000..57cdd693e --- /dev/null +++ b/src/course-outline/header-navigations/HeaderNavigations.jsx @@ -0,0 +1,100 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Button, OverlayTrigger, Tooltip } from '@edx/paragon'; +import { + Add as IconAdd, + ArrowDropDown as ArrowDownIcon, + ArrowDropUp as ArrowUpIcon, +} from '@edx/paragon/icons'; + +import messages from './messages'; + +const HeaderNavigations = ({ + headerNavigationsActions, + isReIndexShow, + isSectionsExpanded, + isDisabledReindexButton, +}) => { + const intl = useIntl(); + const { + handleNewSection, handleReIndex, handleExpandAll, lmsLink, + } = headerNavigationsActions; + + return ( + + ); +}; + +HeaderNavigations.propTypes = { + isReIndexShow: PropTypes.bool.isRequired, + isSectionsExpanded: PropTypes.bool.isRequired, + isDisabledReindexButton: PropTypes.bool.isRequired, + headerNavigationsActions: PropTypes.shape({ + handleNewSection: PropTypes.func.isRequired, + handleReIndex: PropTypes.func.isRequired, + handleExpandAll: PropTypes.func.isRequired, + lmsLink: PropTypes.string.isRequired, + }).isRequired, +}; + +export default HeaderNavigations; diff --git a/src/course-outline/header-navigations/HeaderNavigations.scss b/src/course-outline/header-navigations/HeaderNavigations.scss new file mode 100644 index 000000000..e5867e806 --- /dev/null +++ b/src/course-outline/header-navigations/HeaderNavigations.scss @@ -0,0 +1,4 @@ +.header-navigations { + display: flex; + gap: .75rem; +} diff --git a/src/course-outline/header-navigations/HeaderNavigations.test.jsx b/src/course-outline/header-navigations/HeaderNavigations.test.jsx new file mode 100644 index 000000000..6b0fde9ca --- /dev/null +++ b/src/course-outline/header-navigations/HeaderNavigations.test.jsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import HeaderNavigations from './HeaderNavigations'; +import messages from './messages'; + +const handleNewSectionMock = jest.fn(); +const handleReIndexMock = jest.fn(); +const handleExpandAllMock = jest.fn(); + +const headerNavigationsActions = { + handleNewSection: handleNewSectionMock, + handleReIndex: handleReIndexMock, + handleExpandAll: handleExpandAllMock, + lmsLink: '', +}; + +const renderComponent = (props) => render( + + + , +); + +describe('', () => { + it('render HeaderNavigations component correctly', () => { + const { getByRole } = renderComponent(); + + expect(getByRole('button', { name: messages.newSectionButton.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: messages.reindexButton.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: messages.expandAllButton.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: messages.viewLiveButton.defaultMessage })).toBeInTheDocument(); + }); + + it('render HeaderNavigations component with isReIndexShow is false correctly', () => { + const { getByRole, queryByRole } = renderComponent({ isReIndexShow: false }); + + expect(getByRole('button', { name: messages.newSectionButton.defaultMessage })).toBeInTheDocument(); + expect(queryByRole('button', { name: messages.reindexButton.defaultMessage })).not.toBeInTheDocument(); + expect(getByRole('button', { name: messages.expandAllButton.defaultMessage })).toBeInTheDocument(); + expect(getByRole('button', { name: messages.viewLiveButton.defaultMessage })).toBeInTheDocument(); + }); + + it('calls the correct handlers when clicking buttons', () => { + const { getByRole } = renderComponent(); + + const newSectionButton = getByRole('button', { name: messages.newSectionButton.defaultMessage }); + fireEvent.click(newSectionButton); + expect(handleNewSectionMock).toHaveBeenCalledTimes(1); + + const reIndexButton = getByRole('button', { name: messages.reindexButton.defaultMessage }); + fireEvent.click(reIndexButton); + expect(handleReIndexMock).toHaveBeenCalledTimes(1); + + const expandAllButton = getByRole('button', { name: messages.expandAllButton.defaultMessage }); + fireEvent.click(expandAllButton); + expect(handleExpandAllMock).toHaveBeenCalledTimes(1); + }); + + it('render collapse button correctly', () => { + const { getByRole } = renderComponent({ + isSectionsExpanded: true, + }); + + expect(getByRole('button', { name: messages.collapseAllButton.defaultMessage })).toBeInTheDocument(); + }); + + it('render expand button correctly', () => { + const { getByRole } = renderComponent({ + isSectionsExpanded: false, + }); + + expect(getByRole('button', { name: messages.expandAllButton.defaultMessage })).toBeInTheDocument(); + }); +}); diff --git a/src/course-outline/header-navigations/messages.js b/src/course-outline/header-navigations/messages.js new file mode 100644 index 000000000..588a2fa5d --- /dev/null +++ b/src/course-outline/header-navigations/messages.js @@ -0,0 +1,38 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + newSectionButton: { + id: 'course-authoring.course-outline.header-navigations.button.new-section', + defaultMessage: 'New section', + }, + newSectionButtonTooltip: { + id: 'course-authoring.course-outline.header-navigations.button.new-section.tooltip', + defaultMessage: 'Click to add a new section', + }, + reindexButton: { + id: 'course-authoring.course-outline.header-navigations.button.reindex', + defaultMessage: 'Reindex', + }, + reindexButtonTooltip: { + id: 'course-authoring.course-outline.header-navigations.button.reindex.tooltip', + defaultMessage: 'Reindex current course', + }, + expandAllButton: { + id: 'course-authoring.course-outline.header-navigations.button.expand-all', + defaultMessage: 'Expand all', + }, + collapseAllButton: { + id: 'course-authoring.course-outline.header-navigations.button.collapse-all', + defaultMessage: 'Collapse all', + }, + viewLiveButton: { + id: 'course-authoring.course-outline.header-navigations.button.view-live', + defaultMessage: 'View live', + }, + viewLiveButtonTooltip: { + id: 'course-authoring.course-outline.header-navigations.button.view-live.tooltip', + defaultMessage: 'Click to open the courseware in the LMS in a new tab', + }, +}); + +export default messages; diff --git a/src/course-outline/hooks.jsx b/src/course-outline/hooks.jsx new file mode 100644 index 000000000..f96b84641 --- /dev/null +++ b/src/course-outline/hooks.jsx @@ -0,0 +1,99 @@ +import { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useToggle } from '@edx/paragon'; + +import { RequestStatus } from '../data/constants'; +import { updateSavingStatus } from './data/slice'; +import { + getLoadingStatus, + getOutlineIndexData, + getSavingStatus, + getStatusBarData, +} from './data/selectors'; +import { + enableCourseHighlightsEmailsQuery, + fetchCourseBestPracticesQuery, + fetchCourseLaunchQuery, + fetchCourseOutlineIndexQuery, + fetchCourseReindexQuery, +} from './data/thunk'; + +const useCourseOutline = ({ courseId }) => { + const dispatch = useDispatch(); + + const { reindexLink, lmsLink } = useSelector(getOutlineIndexData); + const { outlineIndexLoadingStatus, reIndexLoadingStatus } = useSelector(getLoadingStatus); + const statusBarData = useSelector(getStatusBarData); + const savingStatus = useSelector(getSavingStatus); + + const [isEnableHighlightsModalOpen, openEnableHighlightsModal, closeEnableHighlightsModal] = useToggle(false); + const [isSectionsExpanded, setSectionsExpanded] = useState(false); + const [isDisabledReindexButton, setDisableReindexButton] = useState(false); + const [showSuccessAlert, setShowSuccessAlert] = useState(false); + const [showErrorAlert, setShowErrorAlert] = useState(false); + + const headerNavigationsActions = { + handleNewSection: () => { + // TODO add handler + }, + handleReIndex: () => { + setDisableReindexButton(true); + setShowSuccessAlert(false); + setShowErrorAlert(false); + + dispatch(fetchCourseReindexQuery(courseId, reindexLink)).then(() => { + setDisableReindexButton(false); + }); + }, + handleExpandAll: () => { + setSectionsExpanded((prevState) => !prevState); + }, + lmsLink, + }; + + const handleEnableHighlightsSubmit = () => { + dispatch(enableCourseHighlightsEmailsQuery(courseId)); + closeEnableHighlightsModal(); + }; + + const handleInternetConnectionFailed = () => { + dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + }; + + useEffect(() => { + dispatch(fetchCourseOutlineIndexQuery(courseId)); + dispatch(fetchCourseBestPracticesQuery({ courseId })); + dispatch(fetchCourseLaunchQuery({ courseId })); + }, [courseId]); + + useEffect(() => { + if (reIndexLoadingStatus === RequestStatus.FAILED) { + setShowErrorAlert(true); + } + + if (reIndexLoadingStatus === RequestStatus.SUCCESSFUL) { + setShowSuccessAlert(true); + } + }, [reIndexLoadingStatus]); + + return { + savingStatus, + isLoading: outlineIndexLoadingStatus === RequestStatus.IN_PROGRESS, + isReIndexShow: Boolean(reindexLink), + showSuccessAlert, + showErrorAlert, + isDisabledReindexButton, + isSectionsExpanded, + headerNavigationsActions, + handleEnableHighlightsSubmit, + statusBarData, + isEnableHighlightsModalOpen, + openEnableHighlightsModal, + closeEnableHighlightsModal, + isInternetConnectionAlertFailed: savingStatus === RequestStatus.FAILED, + handleInternetConnectionFailed, + }; +}; + +// eslint-disable-next-line import/prefer-default-export +export { useCourseOutline }; diff --git a/src/course-outline/index.js b/src/course-outline/index.js new file mode 100644 index 000000000..fbb90f3e6 --- /dev/null +++ b/src/course-outline/index.js @@ -0,0 +1,2 @@ +/* eslint-disable import/prefer-default-export */ +export { default as CourseOutline } from './CourseOutline'; diff --git a/src/course-outline/messages.js b/src/course-outline/messages.js new file mode 100644 index 000000000..387b7f8de --- /dev/null +++ b/src/course-outline/messages.js @@ -0,0 +1,34 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + headingTitle: { + id: 'course-authoring.course-outline.headingTitle', + defaultMessage: 'Course outline', + }, + headingSubtitle: { + id: 'course-authoring.course-outline.subTitle', + defaultMessage: 'Content', + }, + alertSuccessTitle: { + id: 'course-authoring.course-outline.reindex.alert.success.title', + defaultMessage: 'Course index', + }, + alertSuccessDescription: { + id: 'course-authoring.course-outline.reindex.alert.success.description', + defaultMessage: 'Course has been successfully reindexed.', + }, + alertSuccessAriaLabelledby: { + id: 'course-authoring.course-outline.reindex.alert.success.aria.labelledby', + defaultMessage: 'alert-confirmation-title', + }, + alertSuccessAriaDescribedby: { + id: 'course-authoring.course-outline.reindex.alert.success.aria.describedby', + defaultMessage: 'alert-confirmation-description', + }, + alertErrorTitle: { + id: 'course-authoring.course-outline.reindex.alert.error.title', + defaultMessage: 'There were errors reindexing course.', + }, +}); + +export default messages; diff --git a/src/course-outline/outline-sidebar/OutlineSidebar.jsx b/src/course-outline/outline-sidebar/OutlineSidebar.jsx new file mode 100644 index 000000000..0440ad6e5 --- /dev/null +++ b/src/course-outline/outline-sidebar/OutlineSidebar.jsx @@ -0,0 +1,66 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Hyperlink } from '@edx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { HelpSidebar } from '../../generic/help-sidebar'; +import { useHelpUrls } from '../../help-urls/hooks'; +import { getFormattedSidebarMessages } from './utils'; + +const OutlineSideBar = ({ courseId }) => { + const intl = useIntl(); + const { + visibility: learnMoreVisibilityUrl, + grading: learnMoreGradingUrl, + outline: learnMoreOutlineUrl, + } = useHelpUrls(['visibility', 'grading', 'outline']); + + const sidebarMessages = getFormattedSidebarMessages( + { + learnMoreGradingUrl, + learnMoreOutlineUrl, + learnMoreVisibilityUrl, + }, + intl, + ); + + return ( + + {sidebarMessages.map(({ title, descriptions, link }, index) => { + const isLastSection = index === sidebarMessages.length - 1; + + return ( +
+

{title}

+ {descriptions.map((description) => ( +

{description}

+ ))} + {Boolean(link) && Boolean(link.href) && ( + + {link.text} + + )} + {!isLastSection &&
} +
+ ); + })} +
+ ); +}; + +OutlineSideBar.propTypes = { + courseId: PropTypes.string.isRequired, +}; + +export default OutlineSideBar; diff --git a/src/course-outline/outline-sidebar/OutlineSidebar.test.jsx b/src/course-outline/outline-sidebar/OutlineSidebar.test.jsx new file mode 100644 index 000000000..0df0095bb --- /dev/null +++ b/src/course-outline/outline-sidebar/OutlineSidebar.test.jsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import MockAdapter from 'axios-mock-adapter'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { initializeMockApp } from '@edx/frontend-platform'; +import { AppProvider } from '@edx/frontend-platform/react'; + +import { helpUrls } from '../../help-urls/__mocks__'; +import { getHelpUrlsApiUrl } from '../../help-urls/data/api'; +import initializeStore from '../../store'; +import OutlineSidebar from './OutlineSidebar'; +import messages from './messages'; + +let axiosMock; +let store; +const mockPathname = '/foo-bar'; +const courseId = '123'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: () => ({ + pathname: mockPathname, + }), +})); + +jest.mock('@edx/frontend-platform/i18n', () => ({ + ...jest.requireActual('@edx/frontend-platform/i18n'), + useIntl: () => ({ + formatMessage: (message) => message.defaultMessage, + }), +})); + +const renderComponent = (props) => render( + + + + + , +); + +describe('', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + store = initializeStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock + .onGet(getHelpUrlsApiUrl()) + .reply(200, helpUrls); + }); + + it('render OutlineSidebar component correctly', async () => { + const { getByText } = renderComponent(); + + await waitFor(() => { + expect(getByText(messages.section_1_title.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.section_1_descriptions_1.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.section_1_descriptions_2.defaultMessage)).toBeInTheDocument(); + + expect(getByText(messages.section_2_title.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.section_2_descriptions_1.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.section_2_link.defaultMessage)).toBeInTheDocument(); + + expect(getByText(messages.section_3_title.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.section_3_descriptions_1.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.section_3_link.defaultMessage)).toBeInTheDocument(); + + expect(getByText(messages.section_4_title.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.section_4_descriptions_1.defaultMessage)).toBeInTheDocument(); + + expect(getByText(messages.section_4_descriptions_2.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.section_4_descriptions_3.defaultMessage)).toBeInTheDocument(); + + expect(getByText(messages.section_4_link.defaultMessage)).toBeInTheDocument(); + }); + }); +}); diff --git a/src/course-outline/outline-sidebar/messages.js b/src/course-outline/outline-sidebar/messages.js new file mode 100644 index 000000000..7ef0cf0cf --- /dev/null +++ b/src/course-outline/outline-sidebar/messages.js @@ -0,0 +1,70 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + section_1_title: { + id: 'course-authoring.course-outline.sidebar.section-1.title', + defaultMessage: 'Creating your course organization', + }, + section_1_descriptions_1: { + id: 'course-authoring.course-outline.sidebar.section-1.descriptions-1', + defaultMessage: 'You add sections, subsections, and units directly in the outline.', + }, + section_1_descriptions_2: { + id: 'course-authoring.course-outline.sidebar.section-1.descriptions-2', + defaultMessage: 'Create a section, then add subsections and units. Open a unit to add course components.', + }, + section_2_title: { + id: 'course-authoring.course-outline.sidebar.section-2.title', + defaultMessage: 'Reorganizing your course', + }, + section_2_descriptions_1: { + id: 'course-authoring.course-outline.sidebar.section-2.descriptions-1', + defaultMessage: 'Drag sections, subsections, and units to new locations in the outline.', + }, + section_2_link: { + id: 'course-authoring.course-outline.sidebar.section-2.link', + defaultMessage: 'Learn more about the course outline', + }, + section_3_title: { + id: 'course-authoring.course-outline.sidebar.section-3.title', + defaultMessage: 'Setting release dates and grading policies', + }, + section_3_descriptions_1: { + id: 'course-authoring.course-outline.sidebar.section-3.descriptions-1', + defaultMessage: 'Select the Configure icon for a section or subsection to set its release date. When you configure a subsection, you can also set the grading policy and due date.', + }, + section_3_link: { + id: 'course-authoring.course-outline.sidebar.section-3.link', + defaultMessage: 'Learn more about grading policy settings', + }, + section_4_title: { + id: 'course-authoring.course-outline.sidebar.section-4.title', + defaultMessage: 'Changing the content learners see', + }, + section_4_descriptions_1: { + id: 'course-authoring.course-outline.sidebar.section-4.descriptions-1', + defaultMessage: 'To publish draft content, select the Publish icon for a section, subsection, or unit.', + }, + section_4_descriptions_2: { + id: 'course-authoring.course-outline.sidebar.section-4.descriptions-2', + defaultMessage: 'To make a section, subsection, or unit unavailable to learners, select the Configure icon for that level, then select the appropriate {hide} option. Grades for hidden sections, subsections, and units are not included in grade calculations.', + }, + section_4_descriptions_2_hide: { + id: 'course-authoring.course-outline.sidebar.section-4.descriptions-2.hide', + defaultMessage: 'Hide', + }, + section_4_descriptions_3: { + id: 'course-authoring.course-outline.sidebar.section-4.descriptions-3', + defaultMessage: 'To hide the content of a subsection from learners after the subsection due date has passed, select the Configure icon for a subsection, then select {hide}. Grades for the subsection remain included in grade calculations.', + }, + section_4_descriptions_3_hide: { + id: 'course-authoring.course-outline.sidebar.section-4.descriptions-3.hide', + defaultMessage: 'Hide content after due date', + }, + section_4_link: { + id: 'course-authoring.course-outline.sidebar.section-4.link', + defaultMessage: 'Learn more about content visibility settings', + }, +}); + +export default messages; diff --git a/src/course-outline/outline-sidebar/utils.jsx b/src/course-outline/outline-sidebar/utils.jsx new file mode 100644 index 000000000..29782a22c --- /dev/null +++ b/src/course-outline/outline-sidebar/utils.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import messages from './messages'; + +/** + * Get formatted sidebar messages for render + * @param {object} docsLinks - Docs links object from store + * @returns {Array<{ + * title: string, + * descriptions: Array, + * link?: { + * text: string, + * href: string + * } + * }>} + */ +const getFormattedSidebarMessages = (docsLinks, intl) => { + const { learnMoreOutlineUrl, learnMoreGradingUrl, learnMoreVisibilityUrl } = docsLinks; + + return [ + { + title: intl.formatMessage(messages.section_1_title), + descriptions: [ + intl.formatMessage(messages.section_1_descriptions_1), + intl.formatMessage(messages.section_1_descriptions_2), + ], + }, + { + title: intl.formatMessage(messages.section_2_title), + descriptions: [ + intl.formatMessage(messages.section_2_descriptions_1), + ], + link: { + text: intl.formatMessage(messages.section_2_link), + href: learnMoreOutlineUrl, + }, + }, + { + title: intl.formatMessage(messages.section_3_title), + descriptions: [ + intl.formatMessage(messages.section_3_descriptions_1), + ], + link: { + text: intl.formatMessage(messages.section_3_link), + href: learnMoreGradingUrl, + }, + }, + { + title: intl.formatMessage(messages.section_4_title), + descriptions: [ + intl.formatMessage(messages.section_4_descriptions_1), + intl.formatMessage( + messages.section_4_descriptions_2, + { hide: {intl.formatMessage(messages.section_4_descriptions_2_hide)} }, + ), + intl.formatMessage( + messages.section_4_descriptions_3, + { hide: {intl.formatMessage(messages.section_4_descriptions_3_hide)} }, + ), + ], + link: { + text: intl.formatMessage(messages.section_4_link), + href: learnMoreVisibilityUrl, + }, + }, + ]; +}; + +// eslint-disable-next-line import/prefer-default-export +export { getFormattedSidebarMessages }; diff --git a/src/course-outline/status-bar/StatusBar.jsx b/src/course-outline/status-bar/StatusBar.jsx new file mode 100644 index 000000000..62d28f043 --- /dev/null +++ b/src/course-outline/status-bar/StatusBar.jsx @@ -0,0 +1,116 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Button, Hyperlink, Stack } from '@edx/paragon'; +import { AppContext } from '@edx/frontend-platform/react'; + +import messages from './messages'; + +const StatusBar = ({ + statusBarData, + isLoading, + courseId, + openEnableHighlightsModal, +}) => { + const intl = useIntl(); + const { config } = useContext(AppContext); + + const { + courseReleaseDate, + highlightsEnabledForMessaging, + highlightsDocUrl, + checklist, + isSelfPaced, + } = statusBarData; + + const { + completedCourseLaunchChecks, + completedCourseBestPracticesChecks, + totalCourseLaunchChecks, + totalCourseBestPracticesChecks, + } = checklist; + + const checkListTitle = `${completedCourseLaunchChecks + completedCourseBestPracticesChecks}/${totalCourseLaunchChecks + totalCourseBestPracticesChecks}`; + const checklistDestination = new URL(`checklists/${courseId}`, config.STUDIO_BASE_URL).href; + const scheduleDestination = new URL(`course/${courseId}/settings/details#schedule`, config.BASE_URL).href; + + if (isLoading) { + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>; + } + + return ( + +
+
{intl.formatMessage(messages.startDateTitle)}
+ + {courseReleaseDate} + +
+
+
{intl.formatMessage(messages.pacingTypeTitle)}
+ + {isSelfPaced + ? intl.formatMessage(messages.pacingTypeSelfPaced) + : intl.formatMessage(messages.pacingTypeInstructorPaced)} + +
+
+
{intl.formatMessage(messages.checklistTitle)}
+ + {checkListTitle} {intl.formatMessage(messages.checklistCompleted)} + +
+
+
{intl.formatMessage(messages.highlightEmailsTitle)}
+
+ {highlightsEnabledForMessaging ? ( + + {intl.formatMessage(messages.highlightEmailsEnabled)} + + ) : ( + + )} + + {intl.formatMessage(messages.highlightEmailsLink)} + +
+
+
+ ); +}; + +StatusBar.propTypes = { + courseId: PropTypes.string.isRequired, + isLoading: PropTypes.bool.isRequired, + openEnableHighlightsModal: PropTypes.func.isRequired, + statusBarData: PropTypes.shape({ + courseReleaseDate: PropTypes.string.isRequired, + isSelfPaced: PropTypes.bool.isRequired, + checklist: PropTypes.shape({ + totalCourseLaunchChecks: PropTypes.number.isRequired, + completedCourseLaunchChecks: PropTypes.number.isRequired, + totalCourseBestPracticesChecks: PropTypes.number.isRequired, + completedCourseBestPracticesChecks: PropTypes.number.isRequired, + }), + highlightsEnabledForMessaging: PropTypes.bool.isRequired, + highlightsDocUrl: PropTypes.string.isRequired, + }).isRequired, +}; + +export default StatusBar; diff --git a/src/course-outline/status-bar/StatusBar.scss b/src/course-outline/status-bar/StatusBar.scss new file mode 100644 index 000000000..873abef83 --- /dev/null +++ b/src/course-outline/status-bar/StatusBar.scss @@ -0,0 +1,12 @@ +.outline-status-bar { + .outline-status-bar__item { + display: flex; + flex-direction: column; + justify-content: space-evenly; + min-height: 3.75rem; + + & h5 { + margin-bottom: 0; + } + } +} diff --git a/src/course-outline/status-bar/StatusBar.test.jsx b/src/course-outline/status-bar/StatusBar.test.jsx new file mode 100644 index 000000000..64cd617a5 --- /dev/null +++ b/src/course-outline/status-bar/StatusBar.test.jsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { initializeMockApp } from '@edx/frontend-platform'; + +import StatusBar from './StatusBar'; +import messages from './messages'; +import initializeStore from '../../store'; + +let store; +const mockPathname = '/foo-bar'; +const courseId = '123'; +const isLoading = false; +const openEnableHighlightsModalMock = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: () => ({ + pathname: mockPathname, + }), +})); + +const statusBarData = { + courseReleaseDate: 'Feb 05, 2013 at 05:00 UTC', + isSelfPaced: true, + checklist: { + totalCourseLaunchChecks: 5, + completedCourseLaunchChecks: 1, + totalCourseBestPracticesChecks: 4, + completedCourseBestPracticesChecks: 1, + }, + highlightsEnabledForMessaging: true, + highlightsDocUrl: 'https://example.com/highlights-doc', +}; + +const renderComponent = (props) => render( + + + + + , +); + +describe('', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + store = initializeStore(); + }); + + it('renders StatusBar component correctly', () => { + const { getByText } = renderComponent(); + + expect(getByText(messages.startDateTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(statusBarData.courseReleaseDate)).toBeInTheDocument(); + + expect(getByText(messages.pacingTypeTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.pacingTypeSelfPaced.defaultMessage)).toBeInTheDocument(); + + expect(getByText(messages.checklistTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(`2/9 ${messages.checklistCompleted.defaultMessage}`)).toBeInTheDocument(); + + expect(getByText(messages.highlightEmailsTitle.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.highlightEmailsLink.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.highlightEmailsEnabled.defaultMessage)).toBeInTheDocument(); + }); + + it('renders StatusBar when isSelfPaced is false', () => { + const { getByText } = renderComponent({ + statusBarData: { + ...statusBarData, + isSelfPaced: false, + }, + }); + + expect(getByText(messages.pacingTypeInstructorPaced.defaultMessage)).toBeInTheDocument(); + }); + + it('calls openEnableHighlightsModal function when the "Enable Highlight Emails" button is clicked', () => { + const { getByRole } = renderComponent({ + statusBarData: { + ...statusBarData, + highlightsEnabledForMessaging: false, + }, + }); + + const enableHighlightsButton = getByRole('button', { name: messages.highlightEmailsButton.defaultMessage }); + fireEvent.click(enableHighlightsButton); + expect(openEnableHighlightsModalMock).toHaveBeenCalledTimes(1); + }); + + it('not render component when isLoading is true', () => { + const { queryByTestId } = renderComponent({ + isLoading: true, + }); + + expect(queryByTestId('outline-status-bar')).not.toBeInTheDocument(); + }); +}); diff --git a/src/course-outline/status-bar/messages.js b/src/course-outline/status-bar/messages.js new file mode 100644 index 000000000..58ddb2bef --- /dev/null +++ b/src/course-outline/status-bar/messages.js @@ -0,0 +1,46 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + startDateTitle: { + id: 'course-authoring.course-outline.status-bar.start-date', + defaultMessage: 'Start date', + }, + pacingTypeTitle: { + id: 'course-authoring.course-outline.status-bar.pacing-type', + defaultMessage: 'Pacing type', + }, + pacingTypeSelfPaced: { + id: 'course-authoring.course-outline.status-bar.pacing-type.self-paced', + defaultMessage: 'Self-paced', + }, + pacingTypeInstructorPaced: { + id: 'course-authoring.course-outline.status-bar.pacing-type.instructor-Paced', + defaultMessage: 'Instructor-paced', + }, + checklistTitle: { + id: 'course-authoring.course-outline.status-bar.checklists', + defaultMessage: 'Checklists', + }, + checklistCompleted: { + id: 'course-authoring.course-outline.status-bar.checklists.completed', + defaultMessage: 'completed', + }, + highlightEmailsTitle: { + id: 'course-authoring.course-outline.status-bar.highlight-emails', + defaultMessage: 'Course highlight emails', + }, + highlightEmailsButton: { + id: 'course-authoring.course-outline.status-bar.highlight-emails.button', + defaultMessage: 'Enable now', + }, + highlightEmailsEnabled: { + id: 'course-authoring.course-outline.status-bar.highlight-emails.enabled', + defaultMessage: 'Enabled', + }, + highlightEmailsLink: { + id: 'course-authoring.course-outline.status-bar.highlight-emails.link', + defaultMessage: 'Learn more', + }, +}); + +export default messages; diff --git a/src/course-outline/utils/courseChecklistValidators.js b/src/course-outline/utils/courseChecklistValidators.js new file mode 100644 index 000000000..20fae67a2 --- /dev/null +++ b/src/course-outline/utils/courseChecklistValidators.js @@ -0,0 +1,106 @@ +/** + * The utilities are taken from the https://github.com/openedx/studio-frontend repository. + * Perform a minor refactoring of the functions while preserving their original functionality. + */ + +export const hasWelcomeMessage = (updates) => updates.hasUpdate; + +export const hasGradingPolicy = (grades) => { + // eslint-disable-next-line no-shadow + const { hasGradingPolicy, sumOfWeights } = grades; + + return hasGradingPolicy && parseFloat(sumOfWeights.toPrecision(2), 10) === 1.0; +}; + +export const hasCertificate = (certificates) => { + // eslint-disable-next-line no-shadow + const { isActivated, hasCertificate } = certificates; + + return isActivated && hasCertificate; +}; + +export const hasDates = (dates) => { + const { hasStartDate, hasEndDate } = dates; + + return hasStartDate && hasEndDate; +}; + +export const hasAssignmentDeadlines = (assignments, dates) => { + const { + totalNumber, + assignmentsWithDatesBeforeStart, + assignmentsWithDatesAfterEnd, + assignmentsWithOraDatesBeforeStart, + assignmentsWithOraDatesAfterEnd, + } = assignments; + + if (!hasDates(dates)) { + return false; + } + if (totalNumber === 0) { + return false; + } + if (assignmentsWithDatesBeforeStart.length > 0) { + return false; + } + if (assignmentsWithDatesAfterEnd.length > 0) { + return false; + } + if (assignmentsWithOraDatesBeforeStart.length > 0) { + return false; + } + if (assignmentsWithOraDatesAfterEnd.length > 0) { + return false; + } + + return true; +}; + +export const hasShortVideoDuration = (videos) => { + const { totalNumber, durations } = videos; + + if (totalNumber === 0) { + return true; + } + if (totalNumber > 0 && durations.median <= 600) { + return true; + } + + return false; +}; + +export const hasMobileFriendlyVideos = (videos) => { + const { totalNumber, numMobileEncoded } = videos; + + if (totalNumber === 0) { + return true; + } + if (totalNumber > 0 && (numMobileEncoded / totalNumber) >= 0.9) { + return true; + } + + return false; +}; + +export const hasDiverseSequences = (subsections) => { + const { totalVisible, numWithOneBlockType } = subsections; + + if (totalVisible === 0) { + return false; + } + if (totalVisible > 0) { + return ((numWithOneBlockType / totalVisible) < 0.2); + } + + return false; +}; + +export const hasWeeklyHighlights = (sections) => { + const { highlightsActiveForCourse, highlightsEnabled } = sections; + + return highlightsActiveForCourse && highlightsEnabled; +}; + +export const hasShortUnitDepth = (units) => units.numBlocks.median <= 3; + +export const hasProctoringEscalationEmail = (proctoring) => proctoring.hasProctoringEscalationEmail; diff --git a/src/course-outline/utils/courseChecklistValidators.test.js b/src/course-outline/utils/courseChecklistValidators.test.js new file mode 100644 index 000000000..401475bc2 --- /dev/null +++ b/src/course-outline/utils/courseChecklistValidators.test.js @@ -0,0 +1,297 @@ +import * as validators from './courseChecklistValidators'; + +describe('courseCheckValidators utility functions', () => { + describe('hasWelcomeMessage', () => { + it('returns true when course run has an update', () => { + expect(validators.hasWelcomeMessage({ hasUpdate: true })).toEqual(true); + }); + + it('returns false when course run does not have an update', () => { + expect(validators.hasWelcomeMessage({ hasUpdate: false })).toEqual(false); + }); + }); + + describe('hasGradingPolicy', () => { + it('returns true when sum of weights is 1', () => { + expect(validators.hasGradingPolicy( + { hasGradingPolicy: true, sumOfWeights: 1 }, + )).toEqual(true); + }); + + it('returns true when sum of weights is not 1 due to floating point approximation (1.00004)', () => { + expect(validators.hasGradingPolicy( + { hasGradingPolicy: true, sumOfWeights: 1.00004 }, + )).toEqual(true); + }); + + it('returns false when sum of weights is not 1', () => { + expect(validators.hasGradingPolicy( + { hasGradingPolicy: true, sumOfWeights: 2 }, + )).toEqual(false); + }); + + it('returns true when hasGradingPolicy is true', () => { + expect(validators.hasGradingPolicy( + { hasGradingPolicy: true, sumOfWeights: 1 }, + )).toEqual(true); + }); + + it('returns false when hasGradingPolicy is false', () => { + expect(validators.hasGradingPolicy( + { hasGradingPolicy: false, sumOfWeights: 1 }, + )).toEqual(false); + }); + }); + + describe('hasCertificate', () => { + it('returns true when certificates are activated and course run has a certificate', () => { + expect(validators.hasCertificate({ isActivated: true, hasCertificate: true })) + .toEqual(true); + }); + + it('returns false when certificates are not activated and course run has a certificate', () => { + expect(validators.hasCertificate({ isActivated: false, hasCertificate: true })) + .toEqual(false); + }); + + it('returns false when certificates are activated and course run does not have a certificate', () => { + expect(validators.hasCertificate({ isActivated: true, hasCertificate: false })) + .toEqual(false); + }); + + it('returns false when certificates are not activated and course run does not have a certificate', () => { + expect(validators.hasCertificate({ isActivated: false, hasCertificate: false })) + .toEqual(false); + }); + }); + + describe('hasDates', () => { + it('returns true when course run has start date and end date', () => { + expect(validators.hasDates({ hasStartDate: true, hasEndDate: true })).toEqual(true); + }); + + it('returns false when course run has no start date and end date', () => { + expect(validators.hasDates({ hasStartDate: false, hasEndDate: true })).toEqual(false); + }); + + it('returns true when course run has start date and no end date', () => { + expect(validators.hasDates({ hasStartDate: true, hasEndDate: false })).toEqual(false); + }); + + it('returns true when course run has no start date and no end date', () => { + expect(validators.hasDates({ hasStartDate: false, hasEndDate: false })).toEqual(false); + }); + }); + + describe('hasAssignmentDeadlines', () => { + it('returns true when a course run has start and end date and all assignments are within range', () => { + expect(validators.hasAssignmentDeadlines( + { + assignmentsWithDatesBeforeStart: 0, + assignmentsWithDatesAfterEnd: 0, + assignmentsWithOraDatesAfterEnd: 0, + assignmentsWithOraDatesBeforeStart: 0, + }, + { + hasStartDate: true, + hasEndDate: true, + }, + )).toEqual(true); + }); + + it('returns false when a course run has no start and no end date', () => { + expect(validators.hasAssignmentDeadlines( + {}, + { + hasStartDate: false, + hasEndDate: false, + }, + )).toEqual(false); + }); + + it('returns false when a course has start and end date and no assignments', () => { + expect(validators.hasAssignmentDeadlines( + { + totalNumber: 0, + }, + { + hasStartDate: true, + hasEndDate: true, + }, + )).toEqual(false); + }); + + it('returns false when a course run has start and end date and assignments before start', () => { + expect(validators.hasAssignmentDeadlines( + { + assignmentsWithDatesBeforeStart: ['test'], + assignmentsWithDatesAfterEnd: 0, + assignmentsWithOraDatesAfterEnd: 0, + assignmentsWithOraDatesBeforeStart: 0, + }, + { + hasStartDate: true, + hasEndDate: true, + }, + )).toEqual(false); + }); + + it('returns false when a course run has start and end date and assignments after end', () => { + expect(validators.hasAssignmentDeadlines( + { + assignmentsWithDatesBeforeStart: 0, + assignmentsWithDatesAfterEnd: ['test'], + assignmentsWithOraDatesAfterEnd: 0, + assignmentsWithOraDatesBeforeStart: 0, + }, + { + hasStartDate: true, + hasEndDate: true, + }, + )).toEqual(false); + }); + }); + + it( + 'returns false when a course run has start and end date and an ora with a date before start', + () => { + expect(validators.hasAssignmentDeadlines( + { + assignmentsWithDatesBeforeStart: 0, + assignmentsWithDatesAfterEnd: 0, + assignmentsWithOraDatesAfterEnd: 0, + assignmentsWithOraDatesBeforeStart: ['test'], + }, + { + hasStartDate: true, + hasEndDate: true, + }, + )).toEqual(false); + }, + ); + + it( + 'returns false when a course run has start and end date and an ora with a date after end', + () => { + expect(validators.hasAssignmentDeadlines( + { + assignmentsWithDatesBeforeStart: 0, + assignmentsWithDatesAfterEnd: 0, + assignmentsWithOraDatesAfterEnd: ['test'], + assignmentsWithOraDatesBeforeStart: 0, + }, + { + hasStartDate: true, + hasEndDate: true, + }, + )).toEqual(false); + }, + ); + + describe('hasShortVideoDuration', () => { + it('returns true if course run has no videos', () => { + expect(validators.hasShortVideoDuration({ totalNumber: 0 })).toEqual(true); + }); + + it('returns true if course run videos have a median duration <= to 600', () => { + expect(validators.hasShortVideoDuration({ totalNumber: 1, durations: { median: 100 } })) + .toEqual(true); + }); + + it('returns true if course run videos have a median duration > to 600', () => { + expect(validators.hasShortVideoDuration({ totalNumber: 10, durations: { median: 700 } })) + .toEqual(false); + }); + }); + + describe('hasMobileFriendlyVideos', () => { + it('returns true if course run has no videos', () => { + expect(validators.hasMobileFriendlyVideos({ totalNumber: 0 })).toEqual(true); + }); + + it('returns true if course run videos are >= 90% mobile friendly', () => { + expect(validators.hasMobileFriendlyVideos({ totalNumber: 10, numMobileEncoded: 9 })) + .toEqual(true); + }); + + it('returns true if course run videos are < 90% mobile friendly', () => { + expect(validators.hasMobileFriendlyVideos({ totalNumber: 10, numMobileEncoded: 8 })) + .toEqual(false); + }); + }); + + describe('hasDiverseSequences', () => { + it('returns true if < 20% of visible subsections have more than one block type', () => { + expect(validators.hasDiverseSequences({ totalVisible: 10, numWithOneBlockType: 1 })) + .toEqual(true); + }); + + it('returns false if no visible subsections', () => { + expect(validators.hasDiverseSequences({ totalVisible: 0 })).toEqual(false); + }); + + it('returns false if >= 20% of visible subsections have more than one block type', () => { + expect(validators.hasDiverseSequences({ totalVisible: 10, numWithOneBlockType: 3 })) + .toEqual(false); + }); + + it('return false if < 0 visible subsections', () => { + expect(validators.hasDiverseSequences({ totalVisible: -1, numWithOneBlockType: 1 })) + .toEqual(false); + }); + }); + + describe('hasWeeklyHighlights', () => { + it('returns true when course run has highlights enabled', () => { + const data = { highlightsActiveForCourse: true, highlightsEnabled: true }; + expect(validators.hasWeeklyHighlights(data)).toEqual(true); + }); + + it('returns false when course run has highlights enabled', () => { + const data = { highlightsActiveForCourse: false, highlightsEnabled: false }; + expect(validators.hasWeeklyHighlights(data)).toEqual(false); + + data.highlightsEnabled = true; + data.highlightsActiveForCourse = false; + expect(validators.hasWeeklyHighlights(data)).toEqual(false); + + data.highlightsEnabled = false; + data.highlightsActiveForCourse = true; + expect(validators.hasWeeklyHighlights(data)).toEqual(false); + }); + }); + + describe('hasShortUnitDepth', () => { + it('returns true when course run has median number of blocks <= 3', () => { + const units = { + numBlocks: { + median: 3, + }, + }; + + expect(validators.hasShortUnitDepth(units)).toEqual(true); + }); + + it('returns false when course run has median number of blocks > 3', () => { + const units = { + numBlocks: { + median: 4, + }, + }; + + expect(validators.hasShortUnitDepth(units)).toEqual(false); + }); + }); + + describe('hasProctoringEscalationEmail', () => { + it('returns true when the course has a proctoring escalation email', () => { + const proctoring = { hasProctoringEscalationEmail: true }; + expect(validators.hasProctoringEscalationEmail(proctoring)).toEqual(true); + }); + + it('returns false when the course does not have a proctoring escalation email', () => { + const proctoring = { hasProctoringEscalationEmail: false }; + expect(validators.hasProctoringEscalationEmail(proctoring)).toEqual(false); + }); + }); +}); diff --git a/src/course-outline/utils/getChecklistForStatusBar.js b/src/course-outline/utils/getChecklistForStatusBar.js new file mode 100644 index 000000000..acab5d7eb --- /dev/null +++ b/src/course-outline/utils/getChecklistForStatusBar.js @@ -0,0 +1,79 @@ +import { LAUNCH_CHECKLIST, BEST_PRACTICES_CHECKLIST } from '../constants'; +import { getChecklistValues, getChecklistValidatedValue } from './getChecklistValues'; + +/** + * Get status bar course launch checklist values + * @param {object} data - course launch data + * @returns { + * totalCourseLaunchChecks: {number}, + * completedCourseLaunchChecks: {number} + * } - total and completed launch checklist items + */ +const getCourseLaunchChecklist = (data) => { + if (Object.keys(data).length > 0) { + const { isSelfPaced, certificates } = data; + + const filteredCourseLaunchChecks = getChecklistValues({ + checklist: LAUNCH_CHECKLIST.data, + isSelfPaced, + hasCertificatesEnabled: certificates.isEnabled, + hasHighlightsEnabled: false, + }); + + const completedCourseLaunchChecks = filteredCourseLaunchChecks.reduce((result, currentValue) => { + const value = getChecklistValidatedValue(data, currentValue.id); + return value ? result + 1 : result; + }, 0); + + return { + totalCourseLaunchChecks: filteredCourseLaunchChecks.length, + completedCourseLaunchChecks, + }; + } + + return { + totalCourseLaunchChecks: 0, + completedCourseLaunchChecks: 0, + }; +}; + +/** + * Get status bar course best practices checklist values + * @param {object} data - course best practices data + * @returns { + * totalCourseBestPracticesChecks: {number}, + * completedCourseBestPracticesChecks: {number} + * } - total and completed launch checklist items + */ +const getCourseBestPracticesChecklist = (data) => { + if (Object.keys(data).length > 0) { + const { isSelfPaced, sections } = data; + + const filteredBestPracticesChecks = getChecklistValues({ + checklist: BEST_PRACTICES_CHECKLIST.data, + isSelfPaced, + hasCertificatesEnabled: false, + hasHighlightsEnabled: sections.highlightsEnadled, + }); + + const completedCourseBestPracticesChecks = filteredBestPracticesChecks.reduce((result, currentValue) => { + const value = getChecklistValidatedValue(data, currentValue.id); + return value ? result + 1 : result; + }, 0); + + return { + totalCourseBestPracticesChecks: filteredBestPracticesChecks.length, + completedCourseBestPracticesChecks, + }; + } + + return { + totalCourseBestPracticesChecks: 0, + completedCourseBestPracticesChecks: 0, + }; +}; + +export { + getCourseLaunchChecklist, + getCourseBestPracticesChecklist, +}; diff --git a/src/course-outline/utils/getChecklistValues.js b/src/course-outline/utils/getChecklistValues.js new file mode 100644 index 000000000..2ddd965ce --- /dev/null +++ b/src/course-outline/utils/getChecklistValues.js @@ -0,0 +1,79 @@ +import { CHECKLIST_FILTERS } from '../constants'; +import * as healthValidators from './courseChecklistValidators'; + +/** + * The utilities are taken from the https://github.com/openedx/studio-frontend repository. + * Perform a minor refactoring of the functions while preserving their original functionality. + */ +const getChecklistValidatedValue = (data, id) => { + const { + updates, + grades, + certificates, + dates, + assignments, + videos, + subsections, + sections, + units, + proctoring, + } = data; + + switch (id) { + case 'welcomeMessage': + return healthValidators.hasWelcomeMessage(updates); + case 'gradingPolicy': + return healthValidators.hasGradingPolicy(grades); + case 'certificate': + return healthValidators.hasCertificate(certificates); + case 'courseDates': + return healthValidators.hasDates(dates); + case 'assignmentDeadlines': + return healthValidators.hasAssignmentDeadlines(assignments, dates); + case 'videoDuration': + return healthValidators.hasShortVideoDuration(videos); + case 'mobileFriendlyVideo': + return healthValidators.hasMobileFriendlyVideos(videos); + case 'diverseSequences': + return healthValidators.hasDiverseSequences(subsections); + case 'weeklyHighlights': + return healthValidators.hasWeeklyHighlights(sections); + case 'unitDepth': + return healthValidators.hasShortUnitDepth(units); + case 'proctoringEmail': + return healthValidators.hasProctoringEscalationEmail(proctoring); + default: + throw new Error(`Unknown validator ${id}.`); + } +}; + +const getChecklistValues = ({ + checklist, + isSelfPaced, + hasCertificatesEnabled, + hasHighlightsEnabled, + needsProctoringEscalationEmail, +}) => { + let filteredCheckList; + + if (isSelfPaced) { + filteredCheckList = checklist.filter(({ pacingTypeFilter }) => pacingTypeFilter === CHECKLIST_FILTERS.ALL + || pacingTypeFilter === CHECKLIST_FILTERS.SELF_PACED); + } else { + filteredCheckList = checklist.filter(({ pacingTypeFilter }) => pacingTypeFilter === CHECKLIST_FILTERS.ALL + || pacingTypeFilter === CHECKLIST_FILTERS.INSTRUCTOR_PACED); + } + + filteredCheckList = filteredCheckList.filter(({ id }) => id !== 'certificate' + || hasCertificatesEnabled); + + filteredCheckList = filteredCheckList.filter(({ id }) => id !== 'weeklyHighlights' + || hasHighlightsEnabled); + + filteredCheckList = filteredCheckList.filter(({ id }) => id !== 'proctoringEmail' + || needsProctoringEscalationEmail); + + return filteredCheckList; +}; + +export { getChecklistValues, getChecklistValidatedValue }; diff --git a/src/course-outline/utils/getChecklistValues.test.js b/src/course-outline/utils/getChecklistValues.test.js new file mode 100644 index 000000000..24ac32e97 --- /dev/null +++ b/src/course-outline/utils/getChecklistValues.test.js @@ -0,0 +1,86 @@ +import { getChecklistValues } from './getChecklistValues'; +import { CHECKLIST_FILTERS } from '../constants'; + +const checklist = [ + { + id: 'welcomeMessage', + pacingTypeFilter: CHECKLIST_FILTERS.ALL, + }, + { + id: 'gradingPolicy', + pacingTypeFilter: CHECKLIST_FILTERS.ALL, + }, + { + id: 'certificate', + pacingTypeFilter: CHECKLIST_FILTERS.ALL, + }, + { + id: 'courseDates', + pacingTypeFilter: CHECKLIST_FILTERS.ALL, + }, + { + id: 'assignmentDeadlines', + pacingTypeFilter: CHECKLIST_FILTERS.INSTRUCTOR_PACED, + }, + { + id: 'weeklyHighlights', + pacingTypeFilter: CHECKLIST_FILTERS.SELF_PACED, + }, + { + id: 'proctoringEmail', + pacingTypeFilter: CHECKLIST_FILTERS.ALL, + }, +]; + +let courseData; +describe('getChecklistValues utility function', () => { + beforeEach(() => { + courseData = { + isSelfPaced: true, + hasCertificatesEnabled: true, + hasHighlightsEnabled: true, + needsProctoringEscalationEmail: true, + }; + }); + it('returns only checklist items with filters ALL and SELF_PACED when isSelfPaced is true', () => { + const filteredChecklist = getChecklistValues({ checklist, ...courseData }); + + filteredChecklist.forEach((( + item => expect(item.pacingTypeFilter === CHECKLIST_FILTERS.ALL + || item.pacingTypeFilter === CHECKLIST_FILTERS.SELF_PACED) + ))); + + expect(filteredChecklist.filter(item => item.pacingTypeFilter === CHECKLIST_FILTERS.ALL).length) + .toEqual(checklist.filter(item => item.pacingTypeFilter === CHECKLIST_FILTERS.ALL).length); + expect(filteredChecklist.filter(item => item.pacingTypeFilter === CHECKLIST_FILTERS.SELF_PACED).length) + .toEqual(checklist.filter(item => item.pacingTypeFilter === CHECKLIST_FILTERS.SELF_PACED).length); + }); + + it('returns only checklist items with filters ALL and INSTRUCTOR_PACED when isSelfPaced is false', () => { + courseData.isSelfPaced = false; + const filteredChecklist = getChecklistValues({ checklist, ...courseData }); + + filteredChecklist.forEach((( + item => expect(item.pacingTypeFilter === CHECKLIST_FILTERS.ALL + || item.pacingTypeFilter === CHECKLIST_FILTERS.INSTRUCTOR_PACED) + ))); + + expect(filteredChecklist.filter(item => item.pacingTypeFilter === CHECKLIST_FILTERS.ALL).length) + .toEqual(checklist.filter(item => item.pacingTypeFilter === CHECKLIST_FILTERS.ALL).length); + expect(filteredChecklist + .filter(item => item.pacingTypeFilter === CHECKLIST_FILTERS.INSTRUCTOR_PACED).length) + .toEqual(checklist.filter(item => item.pacingTypeFilter === CHECKLIST_FILTERS.INSTRUCTOR_PACED).length); + }); + + it('excludes weekly highlights when they are disabled', () => { + courseData.hasHighlightsEnabled = false; + const filteredChecklist = getChecklistValues({ checklist, ...courseData }); + expect(filteredChecklist.filter(item => item.id === 'weeklyHighlights').length).toEqual(0); + }); + + it('excludes proctoring escalation email when not needed', () => { + courseData.needsProctoringEscalationEmail = false; + const filteredChecklist = getChecklistValues({ checklist, ...courseData }); + expect(filteredChecklist.filter(item => item.id === 'proctoringEmail').length).toEqual(0); + }); +}); diff --git a/src/help-urls/__mocks__/helpUrls.js b/src/help-urls/__mocks__/helpUrls.js new file mode 100644 index 000000000..500c55817 --- /dev/null +++ b/src/help-urls/__mocks__/helpUrls.js @@ -0,0 +1,35 @@ +module.exports = { + default: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/index.html', + home: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/getting_started/CA_get_started_Studio.html', + develop_course: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/index.html', + outline: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_outline.html', + unit: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_units.html', + visibility: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/controlling_content_visibility.html', + updates: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/handouts_updates.html', + pages: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/pages.html', + files: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/course_files.html', + textbooks: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/textbooks.html', + schedule: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/set_up_course/studio_add_course_information/index.html', + grading: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/grading/index.html', + team_course: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/set_up_course/studio_add_course_information/studio_course_staffing.html', + team_library: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_components/libraries.html#give-other-users-access-to-your-library', + advanced: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/index.html', + checklist: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/set_up_course/index.html', + import_library: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_components/libraries.html#import-a-library', + import_course: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/releasing_course/export_import_course.html#import-a-course', + export_library: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_components/libraries.html#export-a-library', + export_course: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/releasing_course/export_import_course.html#export-a-course', + welcome: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/getting_started/index.html', + login: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/getting_started/index.html', + register: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/getting_started/index.html', + content_libraries: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_components/libraries.html', + content_groups: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_features/cohorts/cohorted_courseware.html', + enrollment_tracks: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_features/diff_content/enroll_track_courseware.html', + group_configurations: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_features/content_experiments/content_experiments_configure.html#set-up-group-configurations-in-edx-studio', + container: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_components.html#components-that-contain-other-components', + video: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/video/index.html', + certificates: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/set_up_course/studio_add_course_information/studio_creating_certificates.html', + content_highlights: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages', + image_accessibility: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/accessibility/best_practices_course_content_dev.html#use-best-practices-for-describing-images', + social_sharing: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/social_sharing.html', +}; diff --git a/src/help-urls/__mocks__/index.js b/src/help-urls/__mocks__/index.js new file mode 100644 index 000000000..5acae0519 --- /dev/null +++ b/src/help-urls/__mocks__/index.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as helpUrls } from './helpUrls'; diff --git a/src/help-urls/data/api.js b/src/help-urls/data/api.js index 3243f5be3..b612aa419 100644 --- a/src/help-urls/data/api.js +++ b/src/help-urls/data/api.js @@ -2,8 +2,10 @@ import { camelCaseObject, getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +export const getHelpUrlsApiUrl = () => `${getConfig().STUDIO_BASE_URL}/api/contentstore/v1/help_urls`; + export async function getHelpUrls() { const { data } = await getAuthenticatedHttpClient() - .get(`${getConfig().STUDIO_BASE_URL}/api/contentstore/v1/help_urls`); + .get(getHelpUrlsApiUrl()); return camelCaseObject(data); } diff --git a/src/hooks.js b/src/hooks.js new file mode 100644 index 000000000..d929bb35d --- /dev/null +++ b/src/hooks.js @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; +import { history } from '@edx/frontend-platform'; + +// eslint-disable-next-line import/prefer-default-export +export const useScrollToHashElement = ({ isLoading }) => { + useEffect(() => { + const currentHash = window.location.hash; + + if (currentHash) { + const element = document.querySelector(currentHash); + + if (element) { + element.scrollIntoView(); + history.replace({ hash: '' }); + } + } + }, [isLoading]); +}; diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index bb66c2a14..79c292c0f 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -976,4 +976,4 @@ "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." -} \ No newline at end of file +} diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 030903219..526e1f518 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -976,4 +976,4 @@ "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." -} \ No newline at end of file +} diff --git a/src/i18n/messages/de_DE.json b/src/i18n/messages/de_DE.json index 19c91690f..ed2621dfe 100644 --- a/src/i18n/messages/de_DE.json +++ b/src/i18n/messages/de_DE.json @@ -976,4 +976,4 @@ "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." -} \ No newline at end of file +} diff --git a/src/i18n/messages/es_419.json b/src/i18n/messages/es_419.json index 9296e7cce..2312d8339 100644 --- a/src/i18n/messages/es_419.json +++ b/src/i18n/messages/es_419.json @@ -976,4 +976,4 @@ "course-authoring.studio-home.verify-email.banner.description": "¡Ya casi terminamos! Para completar su registro, necesitamos que verifique su dirección de correo electrónico ({email}). Un mensaje de activación y los pasos a seguir le estarán esperando allí.", "course-authoring.studio-home.verify-email.sidebar.title": "¿Necesita ayuda?", "course-authoring.studio-home.verify-email.sidebar.description": "Por favor revise su correo no desado en caso de que nuestro correo no esté en su buzón de entrada. ¿Aún no encuentra el correo de verificación? Pida ayuda a través del vínculo siguiente." -} \ No newline at end of file +} diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index d9629a30b..786d42f27 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -976,4 +976,4 @@ "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." -} \ No newline at end of file +} diff --git a/src/i18n/messages/fr_CA.json b/src/i18n/messages/fr_CA.json index 201ab0edf..e3e4b7a3a 100644 --- a/src/i18n/messages/fr_CA.json +++ b/src/i18n/messages/fr_CA.json @@ -976,4 +976,4 @@ "course-authoring.studio-home.verify-email.banner.description": "Presque là! Afin de finaliser votre inscription, nous avons besoin que vous vérifiiez votre adresse courriel ({email}). Un message d’activation et les prochaines étapes devraient vous y attendre.", "course-authoring.studio-home.verify-email.sidebar.title": "Besoin d'aide?", "course-authoring.studio-home.verify-email.sidebar.description": "Merci de vérifier votre corbeille ou votre dossier de pourriel au cas où notre courriel ne se trouve pas dans votre boite de réception. Vous ne trouvez toujours pas le courriel de vérification? Demandez de l'aide via le lien ci-dessous." -} \ No newline at end of file +} diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 030903219..526e1f518 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -976,4 +976,4 @@ "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." -} \ No newline at end of file +} diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 030903219..526e1f518 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -976,4 +976,4 @@ "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." -} \ No newline at end of file +} diff --git a/src/i18n/messages/it_IT.json b/src/i18n/messages/it_IT.json index 95f33456f..0072e6110 100644 --- a/src/i18n/messages/it_IT.json +++ b/src/i18n/messages/it_IT.json @@ -976,4 +976,4 @@ "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." -} \ No newline at end of file +} diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 030903219..526e1f518 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -976,4 +976,4 @@ "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." -} \ No newline at end of file +} diff --git a/src/i18n/messages/pt_PT.json b/src/i18n/messages/pt_PT.json index 35de444aa..97514b1a9 100644 --- a/src/i18n/messages/pt_PT.json +++ b/src/i18n/messages/pt_PT.json @@ -976,4 +976,4 @@ "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." -} \ No newline at end of file +} diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 030903219..526e1f518 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -976,4 +976,4 @@ "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." -} \ No newline at end of file +} diff --git a/src/i18n/messages/uk.json b/src/i18n/messages/uk.json index 030903219..526e1f518 100644 --- a/src/i18n/messages/uk.json +++ b/src/i18n/messages/uk.json @@ -976,4 +976,4 @@ "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." -} \ No newline at end of file +} diff --git a/src/i18n/messages/zh_CN.json b/src/i18n/messages/zh_CN.json index 030903219..526e1f518 100644 --- a/src/i18n/messages/zh_CN.json +++ b/src/i18n/messages/zh_CN.json @@ -976,4 +976,4 @@ "course-authoring.studio-home.verify-email.banner.description": "Almost there! In order to complete your sign up we need you to verify your email address ({email}). An activation message and next steps should be waiting for you there.", "course-authoring.studio-home.verify-email.sidebar.title": "Need help?", "course-authoring.studio-home.verify-email.sidebar.description": "Please check your Junk or Spam folders in case our email isn't in your INBOX. Still can't find the verification email? Request help via the link below." -} \ No newline at end of file +} diff --git a/src/index.scss b/src/index.scss index a661c3da2..6b7a1f42a 100755 --- a/src/index.scss +++ b/src/index.scss @@ -21,3 +21,4 @@ @import "taxonomy/taxonomy-card/TaxonomyCard"; @import "files-and-videos"; @import "content-tags-drawer/TagBubble"; +@import "course-outline/CourseOutline"; diff --git a/src/schedule-and-details/index.jsx b/src/schedule-and-details/index.jsx index 42e9893e1..06b66acbb 100644 --- a/src/schedule-and-details/index.jsx +++ b/src/schedule-and-details/index.jsx @@ -16,6 +16,8 @@ import { useModel } from '../generic/model-store'; import AlertMessage from '../generic/alert-message'; import InternetConnectionAlert from '../generic/internet-connection-alert'; import { STATEFUL_BUTTON_STATES } from '../constants'; +import getPageHeadTitle from '../generic/utils'; +import { useScrollToHashElement } from '../hooks'; import { fetchCourseSettingsQuery, fetchCourseDetailsQuery, @@ -40,7 +42,6 @@ import LicenseSection from './license-section'; import ScheduleSidebar from './schedule-sidebar'; import messages from './messages'; import { useSaveValuesPrompt } from './hooks'; -import getPageHeadTitle from '../generic/utils'; const ScheduleAndDetails = ({ intl, courseId }) => { const courseSettings = useSelector(getCourseSettings); @@ -133,6 +134,8 @@ const ScheduleAndDetails = ({ intl, courseId }) => { dispatch(fetchCourseDetailsQuery(courseId)); }, [courseId]); + useScrollToHashElement({ isLoading }); + if (isLoading) { // eslint-disable-next-line react/jsx-no-useless-fragment return <>; diff --git a/src/schedule-and-details/requirements-section/entrance-exam/index.jsx b/src/schedule-and-details/requirements-section/entrance-exam/index.jsx index a4064d5e5..9f33d4be0 100644 --- a/src/schedule-and-details/requirements-section/entrance-exam/index.jsx +++ b/src/schedule-and-details/requirements-section/entrance-exam/index.jsx @@ -19,7 +19,7 @@ const EntranceExam = ({ const toggleEntranceExam = () => onChange((!showEntranceExam).toString(), 'entranceExamEnabled'); const courseOutlineDestination = getPagePath( courseId, - process.env.ENABLE_NEW_COURSE_OUTLINE_PAGE, + 'false', 'course', ); diff --git a/src/schedule-and-details/schedule-section/index.jsx b/src/schedule-and-details/schedule-section/index.jsx index 3af55b129..e7098b392 100644 --- a/src/schedule-and-details/schedule-section/index.jsx +++ b/src/schedule-and-details/schedule-section/index.jsx @@ -109,7 +109,7 @@ const ScheduleSection = ({ ]; return ( -
+