feat: course outline page (#694)

* feat: Course outline Top level page (#36)

* feat: [2u-259] add components

* feat: [2u-259] fix sidebar

* feat: [2u-259] add tests, fix links

* feat: [2u-259] fix messages

* feat: [2u-159] fix reducer and sidebar

* feat: [2u-259] fix reducer

* feat: [2u-259] remove warning from selectors

* feat: [2u-259] remove indents

---------

Co-authored-by: Vladislav Keblysh <vladislavkeblysh@Vladislavs-MacBook-Pro.local>

feat: Course outline Status Bar (#50)

* feat: [2u-259] add components

* feat: [2u-259] fix sidebar

* feat: [2u-259] add tests, fix links

* feat: [2u-259] fix messages

* feat: [2u-159] fix reducer and sidebar

* feat: add checklist

* feat: [2u-259] fix reducer

* feat: [2u-259] remove warning from selectors

* feat: [2u-259] remove indents

* feat: [2u-259] add api, enable modal

* feat: [2u-259] add tests

* feat: [2u-259] add translates

* feat: [2u-271] fix transalates

* feat: [2u-281] fix isQuery pending, utils, hooks

* feat: [2u-281] fix useScrollToHashElement

* feat: [2u-271] fix imports

---------

Co-authored-by: Vladislav Keblysh <vladislavkeblysh@Vladislavs-MacBook-Pro.local>

feat: Course Outline Reindex (#55)

* feat: [2u-277] add alerts

* feat: [2u-277] add translates

* feat: [2u-277] fix tests

* fix: [2u-277] fix slice and hook

---------

Co-authored-by: Vladislav Keblysh <vladislavkeblysh@Vladislavs-MacBook-Pro.local>

fix: Course outline tests (#56)

* fix: fixed course outline status bar tests

* fix: fixed course outline status bar tests

* fix: fixed course outline enable highlights modal tests

* fix: enable modal tests

fix: increase code coverage on the page

* refactor: improve course outline page

feat: lms live link

chore: update outline link

fix: course outline link

refactor: remove unnecessary css and rename test file

refactor: remove unnecessary css from outlineSidebar

test: make use of message variable instead of hardcoded text

refactor: remove unnecessary h5 class

test: use test id for detecting component

refactor: update course outline url and some default messages

---------

Co-authored-by: vladislavkeblysh <138868841+vladislavkeblysh@users.noreply.github.com>
This commit is contained in:
Navin Karkera
2023-12-06 20:36:29 +05:30
committed by GitHub
parent bebbc1535b
commit 04c14274fd
61 changed files with 5507 additions and 21 deletions

View File

@@ -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

View File

@@ -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 = () => {
<CourseAuthoringPage courseId={courseId}>
<Routes>
<Route
path="outline"
element={process.env.ENABLE_NEW_COURSE_OUTLINE_PAGE === 'true' ? <PageWrap><Placeholder /></PageWrap> : null}
path="/"
element={<PageWrap><CourseOutline courseId={courseId} /></PageWrap>}
/>
<Route
path="course_info"

View File

@@ -0,0 +1,141 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Container,
Layout,
TransitionReplace,
} from '@edx/paragon';
import {
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
} from '@edx/paragon/icons';
import SubHeader from '../generic/sub-header/SubHeader';
import { RequestStatus } from '../data/constants';
import InternetConnectionAlert from '../generic/internet-connection-alert';
import AlertMessage from '../generic/alert-message';
import HeaderNavigations from './header-navigations/HeaderNavigations';
import OutlineSideBar from './outline-sidebar/OutlineSidebar';
import messages from './messages';
import { useCourseOutline } from './hooks';
import StatusBar from './status-bar/StatusBar';
import EnableHighlightsModal from './enable-highlights-modal/EnableHighlightsModal';
const CourseOutline = ({ courseId }) => {
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 (
<>
<Container size="xl" className="px-4">
<section className="course-outline-container mb-4 mt-5">
<TransitionReplace>
{showSuccessAlert ? (
<AlertMessage
key={intl.formatMessage(messages.alertSuccessAriaLabelledby)}
show={showSuccessAlert}
variant="success"
icon={CheckCircleIcon}
title={intl.formatMessage(messages.alertSuccessTitle)}
description={intl.formatMessage(messages.alertSuccessDescription)}
aria-hidden="true"
aria-labelledby={intl.formatMessage(messages.alertSuccessAriaLabelledby)}
aria-describedby={intl.formatMessage(messages.alertSuccessAriaDescribedby)}
/>
) : null}
</TransitionReplace>
<SubHeader
className="mt-5"
title={intl.formatMessage(messages.headingTitle)}
subtitle={intl.formatMessage(messages.headingSubtitle)}
headerActions={(
<HeaderNavigations
isReIndexShow={isReIndexShow}
isSectionsExpanded={isSectionsExpanded}
headerNavigationsActions={headerNavigationsActions}
isDisabledReindexButton={isDisabledReindexButton}
/>
)}
/>
<Layout
lg={[{ span: 9 }, { span: 3 }]}
md={[{ span: 9 }, { span: 3 }]}
sm={[{ span: 12 }, { span: 12 }]}
xs={[{ span: 12 }, { span: 12 }]}
xl={[{ span: 9 }, { span: 3 }]}
>
<Layout.Element>
<article>
<div>
<section className="course-outline-section">
<StatusBar
courseId={courseId}
isLoading={isLoading}
statusBarData={statusBarData}
openEnableHighlightsModal={openEnableHighlightsModal}
/>
</section>
</div>
</article>
</Layout.Element>
<Layout.Element>
<OutlineSideBar courseId={courseId} />
</Layout.Element>
</Layout>
<EnableHighlightsModal
isOpen={isEnableHighlightsModalOpen}
close={closeEnableHighlightsModal}
onEnableHighlightsSubmit={handleEnableHighlightsSubmit}
highlightsDocUrl={statusBarData.highlightsDocUrl}
/>
</section>
</Container>
<div className="alert-toast">
<InternetConnectionAlert
isFailed={isInternetConnectionAlertFailed}
isQueryPending={savingStatus === RequestStatus.PENDING}
onInternetConnectionFailed={handleInternetConnectionFailed}
/>
{showErrorAlert && (
<AlertMessage
key={intl.formatMessage(messages.alertErrorTitle)}
show={showErrorAlert}
variant="danger"
icon={WarningIcon}
title={intl.formatMessage(messages.alertErrorTitle)}
aria-hidden="true"
/>
)}
</div>
</>
);
};
CourseOutline.propTypes = {
courseId: PropTypes.string.isRequired,
};
export default CourseOutline;

View File

@@ -0,0 +1,2 @@
@import "./header-navigations/HeaderNavigations";
@import "./status-bar/StatusBar";

View File

@@ -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 = () => (
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseOutline courseId={courseId} />
</IntlProvider>
</AppProvider>
);
describe('<CourseOutline />', () => {
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(<RootWrapper />);
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(<RootWrapper />);
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(<RootWrapper />);
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(<RootWrapper />);
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(<RootWrapper />);
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();
});
});

View File

@@ -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,
},
},
};

View File

@@ -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,
},
};

View File

@@ -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,
};

View File

@@ -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,
};

View File

@@ -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';

View File

@@ -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,
},
],
};

View File

@@ -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<Object>}
*/
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<Object>}
*/
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<Object>}
*/
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<Object>}
*/
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<Object>}
*/
export async function restartIndexingOnCourse(reindexLink) {
const { data } = await getAuthenticatedHttpClient()
.get(getCourseReindexApiUrl(reindexLink));
return camelCaseObject(data);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 }));
}
};
}

View File

@@ -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 (
<AlertModal
title={intl.formatMessage(messages.title)}
variant="default"
size="lg"
isOpen={isOpen}
onClose={close}
footerNode={(
<ActionRow>
<Button variant="tertiary" onClick={close}>
{intl.formatMessage(messages.cancelButton)}
</Button>
<Button onClick={onEnableHighlightsSubmit}>
{intl.formatMessage(messages.submitButton)}
</Button>
</ActionRow>
)}
>
<p className="small">{intl.formatMessage(messages.description_1)}</p>
<p className="small">
{intl.formatMessage(messages.description_2)}
<Hyperlink
className="small ml-2 text-decoration-none"
destination={highlightsDocUrl}
target="_blank"
showLaunchIcon={false}
>
{intl.formatMessage(messages.link)}
</Hyperlink>
</p>
</AlertModal>
);
};
EnableHighlightsModal.propTypes = {
onEnableHighlightsSubmit: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
close: PropTypes.func.isRequired,
highlightsDocUrl: PropTypes.string.isRequired,
};
export default EnableHighlightsModal;

View File

@@ -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(
<IntlProvider locale="en">
<EnableHighlightsModal
isOpen
close={closeMock}
onEnableHighlightsSubmit={onEnableHighlightsSubmitMock}
highlightsDocUrl={highlightsDocUrl}
{...props}
/>
</IntlProvider>,
);
describe('<EnableHighlightsModal />', () => {
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();
});
});

View File

@@ -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;

View File

@@ -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 (
<nav className="header-navigations ml-auto">
<OverlayTrigger
placement="bottom"
overlay={(
<Tooltip id={intl.formatMessage(messages.newSectionButtonTooltip)}>
{intl.formatMessage(messages.newSectionButtonTooltip)}
</Tooltip>
)}
>
<Button
iconBefore={IconAdd}
onClick={handleNewSection}
>
{intl.formatMessage(messages.newSectionButton)}
</Button>
</OverlayTrigger>
{isReIndexShow && (
<OverlayTrigger
placement="bottom"
overlay={!isDisabledReindexButton ? (
<Tooltip id={intl.formatMessage(messages.reindexButtonTooltip)}>
{intl.formatMessage(messages.reindexButtonTooltip)}
</Tooltip>
) : <React.Fragment key="reindex close" />}
>
<Button
onClick={handleReIndex}
variant="outline-primary"
disabled={isDisabledReindexButton}
>
{intl.formatMessage(messages.reindexButton)}
</Button>
</OverlayTrigger>
)}
<Button
variant="outline-primary"
iconBefore={isSectionsExpanded ? ArrowUpIcon : ArrowDownIcon}
onClick={handleExpandAll}
>
{isSectionsExpanded
? intl.formatMessage(messages.collapseAllButton)
: intl.formatMessage(messages.expandAllButton)}
</Button>
<OverlayTrigger
placement="bottom"
overlay={(
<Tooltip id={intl.formatMessage(messages.viewLiveButtonTooltip)}>
{intl.formatMessage(messages.viewLiveButtonTooltip)}
</Tooltip>
)}
>
<Button
href={lmsLink}
target="_blank"
variant="outline-primary"
>
{intl.formatMessage(messages.viewLiveButton)}
</Button>
</OverlayTrigger>
</nav>
);
};
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;

View File

@@ -0,0 +1,4 @@
.header-navigations {
display: flex;
gap: .75rem;
}

View File

@@ -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(
<IntlProvider locale="en">
<HeaderNavigations
headerNavigationsActions={headerNavigationsActions}
isSectionsExpanded={false}
isDisabledReindexButton={false}
isReIndexShow
{...props}
/>
</IntlProvider>,
);
describe('<HeaderNavigations />', () => {
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();
});
});

View File

@@ -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;

View File

@@ -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 };

View File

@@ -0,0 +1,2 @@
/* eslint-disable import/prefer-default-export */
export { default as CourseOutline } from './CourseOutline';

View File

@@ -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;

View File

@@ -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 (
<HelpSidebar
intl={intl}
courseId={courseId}
showOtherSettings={false}
className="outline-sidebar mt-4"
data-testid="outline-sidebar"
>
{sidebarMessages.map(({ title, descriptions, link }, index) => {
const isLastSection = index === sidebarMessages.length - 1;
return (
<div className="outline-sidebar-section" key={title}>
<h4 className="help-sidebar-about-title">{title}</h4>
{descriptions.map((description) => (
<p className="help-sidebar-about-descriptions" key={description}>{description}</p>
))}
{Boolean(link) && Boolean(link.href) && (
<Hyperlink
className="small"
destination={link.href}
target="_blank"
showLaunchIcon={false}
>
{link.text}
</Hyperlink>
)}
{!isLastSection && <hr className="my-3.5" />}
</div>
);
})}
</HelpSidebar>
);
};
OutlineSideBar.propTypes = {
courseId: PropTypes.string.isRequired,
};
export default OutlineSideBar;

View File

@@ -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(
<AppProvider store={store} messages={{}}>
<IntlProvider locale="en">
<OutlineSidebar courseId={courseId} {...props} />
</IntlProvider>
</AppProvider>,
);
describe('<OutlineSidebar />', () => {
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();
});
});
});

View File

@@ -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;

View File

@@ -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<string>,
* 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: <strong>{intl.formatMessage(messages.section_4_descriptions_2_hide)}</strong> },
),
intl.formatMessage(
messages.section_4_descriptions_3,
{ hide: <strong>{intl.formatMessage(messages.section_4_descriptions_3_hide)}</strong> },
),
],
link: {
text: intl.formatMessage(messages.section_4_link),
href: learnMoreVisibilityUrl,
},
},
];
};
// eslint-disable-next-line import/prefer-default-export
export { getFormattedSidebarMessages };

View File

@@ -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 (
<Stack direction="horizontal" gap={3.5} className="outline-status-bar" data-testid="outline-status-bar">
<div className="outline-status-bar__item">
<h5>{intl.formatMessage(messages.startDateTitle)}</h5>
<Hyperlink
className="small"
destination={scheduleDestination}
showLaunchIcon={false}
>
{courseReleaseDate}
</Hyperlink>
</div>
<div className="outline-status-bar__item">
<h5>{intl.formatMessage(messages.pacingTypeTitle)}</h5>
<span className="small">
{isSelfPaced
? intl.formatMessage(messages.pacingTypeSelfPaced)
: intl.formatMessage(messages.pacingTypeInstructorPaced)}
</span>
</div>
<div className="outline-status-bar__item mr-4">
<h5>{intl.formatMessage(messages.checklistTitle)}</h5>
<Hyperlink
className="small"
destination={checklistDestination}
showLaunchIcon={false}
>
{checkListTitle} {intl.formatMessage(messages.checklistCompleted)}
</Hyperlink>
</div>
<div className="outline-status-bar__item ml-4">
<h5>{intl.formatMessage(messages.highlightEmailsTitle)}</h5>
<div className="d-flex align-items-end">
{highlightsEnabledForMessaging ? (
<span data-testid="highlights-enabled-span" className="small">
{intl.formatMessage(messages.highlightEmailsEnabled)}
</span>
) : (
<Button size="sm" onClick={openEnableHighlightsModal}>
{intl.formatMessage(messages.highlightEmailsButton)}
</Button>
)}
<Hyperlink
className="small ml-2"
destination={highlightsDocUrl}
target="_blank"
showLaunchIcon={false}
>
{intl.formatMessage(messages.highlightEmailsLink)}
</Hyperlink>
</div>
</div>
</Stack>
);
};
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;

View File

@@ -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;
}
}
}

View File

@@ -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(
<AppProvider store={store} messages={{}}>
<IntlProvider locale="en">
<StatusBar
courseId={courseId}
isLoading={isLoading}
openEnableHighlightsModal={openEnableHighlightsModalMock}
statusBarData={statusBarData}
{...props}
/>
</IntlProvider>
</AppProvider>,
);
describe('<StatusBar />', () => {
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();
});
});

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
});
});
});

View File

@@ -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,
};

View File

@@ -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 };

View File

@@ -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);
});
});

View File

@@ -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',
};

View File

@@ -0,0 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export { default as helpUrls } from './helpUrls';

View File

@@ -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);
}

18
src/hooks.js Normal file
View File

@@ -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]);
};

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -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 dactivation 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."
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -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."
}
}

View File

@@ -21,3 +21,4 @@
@import "taxonomy/taxonomy-card/TaxonomyCard";
@import "files-and-videos";
@import "content-tags-drawer/TagBubble";
@import "course-outline/CourseOutline";

View File

@@ -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 <></>;

View File

@@ -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',
);

View File

@@ -109,7 +109,7 @@ const ScheduleSection = ({
];
return (
<section className="section-container schedule-section">
<section className="section-container schedule-section" id="schedule">
<SectionSubHeader
title={intl.formatMessage(messages.scheduleTitle)}
description={intl.formatMessage(messages.scheduleDescription)}

View File

@@ -19,6 +19,7 @@ import { reducer as courseExportReducer } from './export-page/data/slice';
import { reducer as genericReducer } from './generic/data/slice';
import { reducer as courseImportReducer } from './import-page/data/slice';
import { reducer as videosReducer } from './files-and-videos/videos-page/data/slice';
import { reducer as courseOutlineReducer } from './course-outline/data/slice';
export default function initializeStore(preloadedState = undefined) {
return configureStore({
@@ -42,6 +43,7 @@ export default function initializeStore(preloadedState = undefined) {
generic: genericReducer,
courseImport: courseImportReducer,
videos: videosReducer,
courseOutline: courseOutlineReducer,
},
preloadedState,
});