diff --git a/.env.development b/.env.development index cc50a024fb..55b8ce70cd 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 0aa1043a6d..6ff7f475bd 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 0000000000..732420365c --- /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 0000000000..b6d98bc470 --- /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 0000000000..494c54d856 --- /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 0000000000..40b629f465 --- /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 0000000000..b65784c9be --- /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 0000000000..89a48492d9 --- /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 0000000000..e699605ef6 --- /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 0000000000..cd4c58eeb4 --- /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 0000000000..9b613bf81c --- /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 0000000000..096723ce5d --- /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 0000000000..ea08b47026 --- /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 0000000000..194ae03d79 --- /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 0000000000..c7e4258aaa --- /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 0000000000..833631d034 --- /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 0000000000..dbad07e941 --- /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 0000000000..57cdd693e2 --- /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 0000000000..e5867e8068 --- /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 0000000000..6b0fde9ca8 --- /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 0000000000..588a2fa5d3 --- /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 0000000000..f96b84641c --- /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 0000000000..fbb90f3e66 --- /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 0000000000..387b7f8ded --- /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 0000000000..0440ad6e52 --- /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 0000000000..0df0095bbb --- /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 0000000000..7ef0cf0cfb --- /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 0000000000..29782a22c6 --- /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 0000000000..62d28f0430 --- /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 0000000000..873abef83b --- /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 0000000000..64cd617a5e --- /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 0000000000..58ddb2bef3 --- /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 0000000000..20fae67a23 --- /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 0000000000..401475bc2f --- /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 0000000000..acab5d7eb3 --- /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 0000000000..2ddd965ce8 --- /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 0000000000..24ac32e973 --- /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 0000000000..500c558176 --- /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 0000000000..5acae05196 --- /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 3243f5be33..b612aa419f 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 0000000000..d929bb35d3 --- /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 bb66c2a14d..79c292c0f3 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 030903219c..526e1f518e 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 19c91690f8..ed2621dfe9 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 9296e7cceb..2312d83390 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 d9629a30bc..786d42f27a 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 201ab0edf5..e3e4b7a3ad 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 030903219c..526e1f518e 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 030903219c..526e1f518e 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 95f33456f3..0072e6110d 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 030903219c..526e1f518e 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 35de444aad..97514b1a97 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 030903219c..526e1f518e 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 030903219c..526e1f518e 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 030903219c..526e1f518e 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 a661c3da23..6b7a1f42ae 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 42e9893e17..06b66acbbf 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 a4064d5e51..9f33d4be0a 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 3af55b129a..e7098b3921 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 ( -
+