feat: [FC-0044] Unit page - Manage access modal (unit & xblocks) (#901)

* feat: [FC-0044] Unit page - Manage access modal (unit & xblocks)

* fix: add message description
This commit is contained in:
Ihor Romaniuk
2024-04-22 17:13:16 +02:00
committed by GitHub
parent 1834655399
commit 6ec44b5f41
37 changed files with 1202 additions and 330 deletions

View File

@@ -0,0 +1,276 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { Alert, Form, Hyperlink } from '@openedx/paragon';
import {
Warning as WarningIcon,
} from '@openedx/paragon/icons';
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
import PrereqSettings from './PrereqSettings';
const AdvancedTab = ({
values,
setFieldValue,
prereqs,
releasedToStudents,
wasExamEverLinkedWithExternal,
enableProctoredExams,
supportsOnboarding,
wasProctoredExam,
showReviewRules,
onlineProctoringRules,
}) => {
const {
isTimeLimited,
isProctoredExam,
isOnboardingExam,
isPracticeExam,
defaultTimeLimitMinutes,
examReviewRules,
} = values;
let examTypeValue = 'none';
if (isTimeLimited && isProctoredExam) {
if (isOnboardingExam) {
examTypeValue = 'onboardingExam';
} else if (isPracticeExam) {
examTypeValue = 'practiceExam';
} else {
examTypeValue = 'proctoredExam';
}
} else if (isTimeLimited) {
examTypeValue = 'timed';
}
const formatHour = (hour) => {
const hh = Math.floor(hour / 60);
const mm = hour % 60;
let hhs = `${hh}`;
let mms = `${mm}`;
if (hh < 10) {
hhs = `0${hh}`;
}
if (mm < 10) {
mms = `0${mm}`;
}
if (Number.isNaN(hh)) {
hhs = '00';
}
if (Number.isNaN(mm)) {
mms = '00';
}
return `${hhs}:${mms}`;
};
const [timeLimit, setTimeLimit] = useState(formatHour(defaultTimeLimitMinutes));
const showReviewRulesDiv = showReviewRules && isProctoredExam && !isPracticeExam && !isOnboardingExam;
const handleChange = (e) => {
if (e.target.value === 'timed') {
setFieldValue('isTimeLimited', true);
setFieldValue('isOnboardingExam', false);
setFieldValue('isPracticeExam', false);
setFieldValue('isProctoredExam', false);
} else if (e.target.value === 'onboardingExam') {
setFieldValue('isOnboardingExam', true);
setFieldValue('isProctoredExam', true);
setFieldValue('isTimeLimited', true);
setFieldValue('isPracticeExam', false);
} else if (e.target.value === 'practiceExam') {
setFieldValue('isPracticeExam', true);
setFieldValue('isProctoredExam', true);
setFieldValue('isTimeLimited', true);
setFieldValue('isOnboardingExam', false);
} else if (e.target.value === 'proctoredExam') {
setFieldValue('isProctoredExam', true);
setFieldValue('isTimeLimited', true);
setFieldValue('isOnboardingExam', false);
setFieldValue('isPracticeExam', false);
} else {
setFieldValue('isTimeLimited', false);
setFieldValue('isOnboardingExam', false);
setFieldValue('isPracticeExam', false);
setFieldValue('isProctoredExam', false);
}
};
const setCurrentTimeLimit = (event) => {
const { validity: { valid } } = event.target;
let { value } = event.target;
value = value.trim();
if (value && valid) {
const minutes = moment.duration(value).asMinutes();
setFieldValue('defaultTimeLimitMinutes', minutes);
}
setTimeLimit(value);
};
const renderAlerts = () => {
const proctoredExamLockedIn = releasedToStudents && wasExamEverLinkedWithExternal;
return (
<>
{proctoredExamLockedIn && !wasProctoredExam && (
<Alert variant="warning" icon={WarningIcon}>
<FormattedMessage {...messages.proctoredExamLockedAndisNotProctoredExamAlert} />
</Alert>
)}
{proctoredExamLockedIn && wasProctoredExam && (
<Alert variant="warning" icon={WarningIcon}>
<FormattedMessage {...messages.proctoredExamLockedAndisProctoredExamAlert} />
</Alert>
)}
</>
);
};
return (
<>
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.setSpecialExam} /></h5>
<hr />
<Form.RadioSet
name="specialExam"
onChange={handleChange}
value={examTypeValue}
>
{renderAlerts()}
<Form.Radio value="none">
<FormattedMessage {...messages.none} />
</Form.Radio>
<Form.Radio
value="timed"
description={<FormattedMessage {...messages.timedDescription} />}
controlClassName="mw-1-25rem"
>
<FormattedMessage {...messages.timed} />
</Form.Radio>
{enableProctoredExams && (
<>
<Form.Radio
value="proctoredExam"
description={<FormattedMessage {...messages.proctoredExamDescription} />}
controlClassName="mw-1-25rem"
>
<FormattedMessage {...messages.proctoredExam} />
</Form.Radio>
{supportsOnboarding ? (
<Form.Radio
description={<FormattedMessage {...messages.onboardingExamDescription} />}
value="onboardingExam"
controlClassName="mw-1-25rem"
>
<FormattedMessage {...messages.onboardingExam} />
</Form.Radio>
) : (
<Form.Radio
value="practiceExam"
controlClassName="mw-1-25rem"
description={<FormattedMessage {...messages.practiceExamDescription} />}
>
<FormattedMessage {...messages.practiceExam} />
</Form.Radio>
)}
</>
)}
</Form.RadioSet>
{ isTimeLimited && (
<div className="mt-3" data-testid="advanced-tab-hours-picker-wrapper">
<Form.Group>
<Form.Label>
<FormattedMessage {...messages.timeAllotted} />
</Form.Label>
<Form.Control
onChange={setCurrentTimeLimit}
value={timeLimit}
placeholder="HH:MM"
pattern="^[0-9][0-9]:[0-5][0-9]$"
/>
</Form.Group>
<Form.Text><FormattedMessage {...messages.timeLimitDescription} /></Form.Text>
</div>
)}
{ showReviewRulesDiv && (
<div className="mt-3">
<Form.Group>
<Form.Label>
<FormattedMessage {...messages.reviewRulesLabel} />
</Form.Label>
<Form.Control
onChange={(e) => setFieldValue('examReviewRules', e.target.value)}
value={examReviewRules}
as="textarea"
rows="3"
/>
</Form.Group>
<Form.Text>
{ onlineProctoringRules ? (
<FormattedMessage
{...messages.reviewRulesDescriptionWithLink}
values={{
hyperlink: (
<Hyperlink
destination={onlineProctoringRules}
target="_blank"
showLaunchIcon={false}
>
<FormattedMessage
{...messages.reviewRulesDescriptionLinkText}
/>
</Hyperlink>
),
}}
/>
) : (
<FormattedMessage {...messages.reviewRulesDescription} />
)}
</Form.Text>
</div>
)}
<PrereqSettings
values={values}
setFieldValue={setFieldValue}
prereqs={prereqs}
/>
</>
);
};
AdvancedTab.defaultProps = {
prereqs: [],
wasExamEverLinkedWithExternal: false,
enableProctoredExams: false,
supportsOnboarding: false,
wasProctoredExam: false,
showReviewRules: false,
onlineProctoringRules: '',
};
AdvancedTab.propTypes = {
values: PropTypes.shape({
isTimeLimited: PropTypes.bool.isRequired,
defaultTimeLimitMinutes: PropTypes.number,
isPrereq: PropTypes.bool,
prereqUsageKey: PropTypes.string,
prereqMinScore: PropTypes.number,
prereqMinCompletion: PropTypes.number,
isProctoredExam: PropTypes.bool,
isPracticeExam: PropTypes.bool,
isOnboardingExam: PropTypes.bool,
examReviewRules: PropTypes.string,
}).isRequired,
setFieldValue: PropTypes.func.isRequired,
prereqs: PropTypes.arrayOf(PropTypes.shape({
blockUsageKey: PropTypes.string.isRequired,
blockDisplayName: PropTypes.string.isRequired,
})),
releasedToStudents: PropTypes.bool.isRequired,
wasExamEverLinkedWithExternal: PropTypes.bool,
enableProctoredExams: PropTypes.bool,
supportsOnboarding: PropTypes.bool,
wasProctoredExam: PropTypes.bool,
showReviewRules: PropTypes.bool,
onlineProctoringRules: PropTypes.string,
};
export default injectIntl(AdvancedTab);

View File

@@ -0,0 +1,106 @@
import PropTypes from 'prop-types';
import { Stack, Form } from '@openedx/paragon';
import { FormattedMessage, injectIntl, useIntl } from '@edx/frontend-platform/i18n';
import { DatepickerControl, DATEPICKER_TYPES } from '../datepicker-control';
import messages from './messages';
const BasicTab = ({
values,
setFieldValue,
courseGraders,
isSubsection,
}) => {
const intl = useIntl();
const {
releaseDate,
graderType,
dueDate,
} = values;
const onChangeGraderType = (e) => setFieldValue('graderType', e.target.value);
const createOptions = () => courseGraders.map((option) => (
<option key={option} value={option}> {option} </option>
));
return (
<>
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.releaseDateAndTime} /></h5>
<hr />
<div data-testid="release-date-stack">
<Stack className="mt-3" direction="horizontal" gap={5}>
<DatepickerControl
type={DATEPICKER_TYPES.date}
value={releaseDate}
label={intl.formatMessage(messages.releaseDate)}
controlName="state-date"
onChange={(val) => setFieldValue('releaseDate', val)}
/>
<DatepickerControl
type={DATEPICKER_TYPES.time}
value={releaseDate}
label={intl.formatMessage(messages.releaseTimeUTC)}
controlName="start-time"
onChange={(val) => setFieldValue('releaseDate', val)}
/>
</Stack>
</div>
{
isSubsection && (
<div>
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.grading} /></h5>
<hr />
<Form.Group>
<Form.Label><FormattedMessage {...messages.gradeAs} /></Form.Label>
<Form.Control
as="select"
defaultValue={graderType}
onChange={onChangeGraderType}
data-testid="grader-type-select"
>
<option key="notgraded" value="notgraded">
{intl.formatMessage(messages.notGradedTypeOption)}
</option>
{createOptions()}
</Form.Control>
</Form.Group>
<div data-testid="due-date-stack">
<Stack className="mt-3" direction="horizontal" gap={5}>
<DatepickerControl
type={DATEPICKER_TYPES.date}
value={dueDate}
label={intl.formatMessage(messages.dueDate)}
controlName="state-date"
onChange={(val) => setFieldValue('dueDate', val)}
data-testid="due-date-picker"
/>
<DatepickerControl
type={DATEPICKER_TYPES.time}
value={dueDate}
label={intl.formatMessage(messages.dueTimeUTC)}
controlName="start-time"
onChange={(val) => setFieldValue('dueDate', val)}
/>
</Stack>
</div>
</div>
)
}
</>
);
};
BasicTab.propTypes = {
isSubsection: PropTypes.bool.isRequired,
values: PropTypes.shape({
releaseDate: PropTypes.string.isRequired,
graderType: PropTypes.string.isRequired,
dueDate: PropTypes.string,
}).isRequired,
courseGraders: PropTypes.arrayOf(PropTypes.string).isRequired,
setFieldValue: PropTypes.func.isRequired,
};
export default injectIntl(BasicTab);

View File

@@ -0,0 +1,365 @@
/* eslint-disable import/named */
import React from 'react';
import * as Yup from 'yup';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
ModalDialog,
Button,
ActionRow,
Form,
Tab,
Tabs,
} from '@openedx/paragon';
import { Formik } from 'formik';
import { VisibilityTypes } from '../../data/constants';
import { COURSE_BLOCK_NAMES } from '../../constants';
import messages from './messages';
import BasicTab from './BasicTab';
import VisibilityTab from './VisibilityTab';
import AdvancedTab from './AdvancedTab';
import UnitTab from './UnitTab';
const ConfigureModal = ({
isOpen,
onClose,
onConfigureSubmit,
currentItemData,
enableProctoredExams,
isXBlockComponent,
}) => {
const intl = useIntl();
const {
displayName,
start: sectionStartDate,
visibilityState,
due,
isTimeLimited,
defaultTimeLimitMinutes,
hideAfterDue,
showCorrectness,
courseGraders,
category,
format,
userPartitionInfo,
ancestorHasStaffLock,
isPrereq,
prereqs,
prereq,
prereqMinScore,
prereqMinCompletion,
releasedToStudents,
wasExamEverLinkedWithExternal,
isProctoredExam,
isOnboardingExam,
isPracticeExam,
examReviewRules,
supportsOnboarding,
showReviewRules,
onlineProctoringRules,
} = currentItemData;
const getSelectedGroups = () => {
if (userPartitionInfo?.selectedPartitionIndex >= 0) {
return userPartitionInfo?.selectablePartitions[userPartitionInfo?.selectedPartitionIndex]
?.groups
.filter(({ selected }) => selected)
.map(({ id }) => `${id}`)
|| [];
}
return [];
};
const defaultPrereqScore = (val) => {
if (val === null || val === undefined) {
return 100;
}
return parseFloat(val);
};
const initialValues = {
releaseDate: sectionStartDate,
isVisibleToStaffOnly: visibilityState === VisibilityTypes.STAFF_ONLY,
graderType: format == null ? 'notgraded' : format,
dueDate: due == null ? '' : due,
isTimeLimited,
isProctoredExam,
isOnboardingExam,
isPracticeExam,
examReviewRules,
defaultTimeLimitMinutes,
hideAfterDue: hideAfterDue === undefined ? false : hideAfterDue,
showCorrectness,
isPrereq,
prereqUsageKey: prereq,
prereqMinScore: defaultPrereqScore(prereqMinScore),
prereqMinCompletion: defaultPrereqScore(prereqMinCompletion),
// by default it is -1 i.e. accessible to all learners & staff
selectedPartitionIndex: userPartitionInfo?.selectedPartitionIndex,
selectedGroups: getSelectedGroups(),
};
const validationSchema = Yup.object().shape({
isTimeLimited: Yup.boolean(),
isProctoredExam: Yup.boolean(),
isPracticeExam: Yup.boolean(),
isOnboardingExam: Yup.boolean(),
examReviewRules: Yup.string(),
defaultTimeLimitMinutes: Yup.number().nullable(true),
hideAfterDueState: Yup.boolean(),
showCorrectness: Yup.string().required(),
isPrereq: Yup.boolean(),
prereqUsageKey: Yup.string().nullable(true),
prereqMinScore: Yup.number().min(
0,
intl.formatMessage(messages.minScoreError),
).max(
100,
intl.formatMessage(messages.minScoreError),
).nullable(true),
prereqMinCompletion: Yup.number().min(
0,
intl.formatMessage(messages.minScoreError),
).max(
100,
intl.formatMessage(messages.minScoreError),
).nullable(true),
selectedPartitionIndex: Yup.number().integer(),
selectedGroups: Yup.array().of(Yup.string()),
});
const isSubsection = category === COURSE_BLOCK_NAMES.sequential.id;
const dialogTitle = isXBlockComponent
? intl.formatMessage(messages.componentTitle, { title: displayName })
: intl.formatMessage(messages.title, { title: displayName });
const handleSave = (data) => {
const groupAccess = {};
switch (category) {
case COURSE_BLOCK_NAMES.chapter.id:
onConfigureSubmit(data.isVisibleToStaffOnly, data.releaseDate);
break;
case COURSE_BLOCK_NAMES.sequential.id:
onConfigureSubmit(
data.isVisibleToStaffOnly,
data.releaseDate,
data.graderType,
data.dueDate,
data.isTimeLimited,
data.isProctoredExam,
data.isOnboardingExam,
data.isPracticeExam,
data.examReviewRules,
data.isTimeLimited ? data.defaultTimeLimitMinutes : 0,
data.hideAfterDue,
data.showCorrectness,
data.isPrereq,
data.prereqUsageKey,
data.prereqMinScore,
data.prereqMinCompletion,
);
break;
case COURSE_BLOCK_NAMES.vertical.id:
case COURSE_BLOCK_NAMES.component.id:
// groupAccess should be {partitionId: [group1, group2]} or {} if selectedPartitionIndex === -1
if (data.selectedPartitionIndex >= 0) {
const partitionId = userPartitionInfo.selectablePartitions[data.selectedPartitionIndex].id;
groupAccess[partitionId] = data.selectedGroups.map(g => parseInt(g, 10));
}
onConfigureSubmit(data.isVisibleToStaffOnly, groupAccess);
break;
default:
break;
}
};
const renderModalBody = (values, setFieldValue) => {
switch (category) {
case COURSE_BLOCK_NAMES.chapter.id:
return (
<Tabs>
<Tab eventKey="basic" title={intl.formatMessage(messages.basicTabTitle)}>
<BasicTab
values={values}
setFieldValue={setFieldValue}
isSubsection={isSubsection}
courseGraders={courseGraders === 'undefined' ? [] : courseGraders}
/>
</Tab>
<Tab eventKey="visibility" title={intl.formatMessage(messages.visibilityTabTitle)}>
<VisibilityTab
values={values}
setFieldValue={setFieldValue}
category={category}
isSubsection={isSubsection}
showWarning={visibilityState === VisibilityTypes.STAFF_ONLY}
/>
</Tab>
</Tabs>
);
case COURSE_BLOCK_NAMES.sequential.id:
return (
<Tabs>
<Tab eventKey="basic" title={intl.formatMessage(messages.basicTabTitle)}>
<BasicTab
values={values}
setFieldValue={setFieldValue}
isSubsection={isSubsection}
courseGraders={courseGraders === 'undefined' ? [] : courseGraders}
/>
</Tab>
<Tab eventKey="visibility" title={intl.formatMessage(messages.visibilityTabTitle)}>
<VisibilityTab
values={values}
setFieldValue={setFieldValue}
category={category}
isSubsection={isSubsection}
showWarning={visibilityState === VisibilityTypes.STAFF_ONLY}
/>
</Tab>
<Tab eventKey="advanced" title={intl.formatMessage(messages.advancedTabTitle)}>
<AdvancedTab
values={values}
setFieldValue={setFieldValue}
prereqs={prereqs}
releasedToStudents={releasedToStudents}
wasExamEverLinkedWithExternal={wasExamEverLinkedWithExternal}
enableProctoredExams={enableProctoredExams}
supportsOnboarding={supportsOnboarding}
showReviewRules={showReviewRules}
wasProctoredExam={isProctoredExam}
onlineProctoringRules={onlineProctoringRules}
/>
</Tab>
</Tabs>
);
case COURSE_BLOCK_NAMES.vertical.id:
case COURSE_BLOCK_NAMES.component.id:
return (
<UnitTab
isXBlockComponent={COURSE_BLOCK_NAMES.component.id === category}
values={values}
setFieldValue={setFieldValue}
showWarning={visibilityState === VisibilityTypes.STAFF_ONLY && !ancestorHasStaffLock}
userPartitionInfo={userPartitionInfo}
/>
);
default:
return null;
}
};
return (
<ModalDialog
className="configure-modal"
size="lg"
isOpen={isOpen}
onClose={onClose}
hasCloseButton
isFullscreenOnMobile
>
<div data-testid="configure-modal">
<ModalDialog.Header className="configure-modal__header">
<ModalDialog.Title>
{dialogTitle}
</ModalDialog.Title>
</ModalDialog.Header>
<Formik
initialValues={initialValues}
onSubmit={handleSave}
validationSchema={validationSchema}
validateOnBlur
validateOnChange
>
{({
values, handleSubmit, setFieldValue,
}) => (
<>
<ModalDialog.Body className="configure-modal__body">
<Form.Group size="sm" className="form-field">
{renderModalBody(values, setFieldValue)}
</Form.Group>
</ModalDialog.Body>
<ModalDialog.Footer className="pt-1">
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
{intl.formatMessage(messages.cancelButton)}
</ModalDialog.CloseButton>
<Button
data-testid="configure-save-button"
onClick={handleSubmit}
>
{intl.formatMessage(messages.saveButton)}
</Button>
</ActionRow>
</ModalDialog.Footer>
</>
)}
</Formik>
</div>
</ModalDialog>
);
};
ConfigureModal.defaultProps = {
isXBlockComponent: false,
enableProctoredExams: false,
};
ConfigureModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onConfigureSubmit: PropTypes.func.isRequired,
enableProctoredExams: PropTypes.bool,
currentItemData: PropTypes.shape({
displayName: PropTypes.string,
start: PropTypes.string,
visibilityState: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
due: PropTypes.string,
isTimeLimited: PropTypes.bool,
defaultTimeLimitMinutes: PropTypes.number,
hideAfterDue: PropTypes.bool,
showCorrectness: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
courseGraders: PropTypes.arrayOf(PropTypes.string),
category: PropTypes.string,
format: PropTypes.string,
userPartitionInfo: PropTypes.shape({
selectablePartitions: PropTypes.arrayOf(PropTypes.shape({
groups: PropTypes.arrayOf(PropTypes.shape({
deleted: PropTypes.bool,
id: PropTypes.number,
name: PropTypes.string,
selected: PropTypes.bool,
})),
id: PropTypes.number,
name: PropTypes.string,
scheme: PropTypes.string,
})),
selectedPartitionIndex: PropTypes.number,
selectedGroupsLabel: PropTypes.string,
}),
ancestorHasStaffLock: PropTypes.bool,
isPrereq: PropTypes.bool,
prereqs: PropTypes.arrayOf({
blockDisplayName: PropTypes.string,
blockUsageKey: PropTypes.string,
}),
prereq: PropTypes.number,
prereqMinScore: PropTypes.number,
prereqMinCompletion: PropTypes.number,
releasedToStudents: PropTypes.bool,
wasExamEverLinkedWithExternal: PropTypes.bool,
isProctoredExam: PropTypes.bool,
isOnboardingExam: PropTypes.bool,
isPracticeExam: PropTypes.bool,
examReviewRules: PropTypes.string,
supportsOnboarding: PropTypes.bool,
showReviewRules: PropTypes.bool,
onlineProctoringRules: PropTypes.string,
}).isRequired,
isXBlockComponent: PropTypes.bool,
};
export default ConfigureModal;

View File

@@ -0,0 +1,14 @@
.configure-modal {
.configure-modal__header {
padding-top: 1.5rem;
position: static;
}
.w-7rem {
width: 7.2rem;
}
.mw-1-25rem {
min-width: 1.25rem;
}
}

View File

@@ -0,0 +1,278 @@
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { initializeMockApp } from '@edx/frontend-platform';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react';
import initializeStore from '../../store';
import ConfigureModal from './ConfigureModal';
import {
currentSectionMock,
currentSubsectionMock,
currentUnitMock,
currentXBlockMock,
} from './__mocks__';
import messages from './messages';
// eslint-disable-next-line no-unused-vars
let axiosMock;
let store;
const mockPathname = '/foo-bar';
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: () => ({
pathname: mockPathname,
}),
}));
const onCloseMock = jest.fn();
const onConfigureSubmitMock = jest.fn();
const renderComponent = () => render(
<AppProvider store={store}>
<IntlProvider locale="en">
<ConfigureModal
isOpen
onClose={onCloseMock}
onConfigureSubmit={onConfigureSubmitMock}
currentItemData={currentSectionMock}
/>
</IntlProvider>,
</AppProvider>,
);
describe('<ConfigureModal /> for Section', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
it('renders ConfigureModal component correctly', () => {
const { getByText, getByRole } = renderComponent();
expect(getByText(`${currentSectionMock.displayName} settings`)).toBeInTheDocument();
expect(getByText(messages.basicTabTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.visibilityTabTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.releaseDate.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.releaseTimeUTC.defaultMessage)).toBeInTheDocument();
expect(getByRole('button', { name: messages.cancelButton.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: messages.saveButton.defaultMessage })).toBeInTheDocument();
});
it('switches to the Visibility tab and renders correctly', () => {
const { getByRole, getByText } = renderComponent();
const visibilityTab = getByRole('tab', { name: messages.visibilityTabTitle.defaultMessage });
userEvent.click(visibilityTab);
expect(getByText('Section visibility')).toBeInTheDocument();
expect(getByText(messages.hideFromLearners.defaultMessage)).toBeInTheDocument();
});
});
const renderSubsectionComponent = () => render(
<AppProvider store={store}>
<IntlProvider locale="en">
<ConfigureModal
isOpen
onClose={onCloseMock}
onConfigureSubmit={onConfigureSubmitMock}
currentItemData={currentSubsectionMock}
/>
</IntlProvider>,
</AppProvider>,
);
describe('<ConfigureModal /> for Subsection', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
it('renders subsection ConfigureModal component correctly', () => {
const { getByText, getByRole } = renderSubsectionComponent();
expect(getByText(`${currentSubsectionMock.displayName} settings`)).toBeInTheDocument();
expect(getByText(messages.basicTabTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.visibilityTabTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.advancedTabTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.releaseDate.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.releaseTimeUTC.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.grading.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.gradeAs.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.dueDate.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.dueTimeUTC.defaultMessage)).toBeInTheDocument();
expect(getByRole('button', { name: messages.cancelButton.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: messages.saveButton.defaultMessage })).toBeInTheDocument();
});
it('switches to the subsection Visibility tab and renders correctly', () => {
const { getByRole, getByText } = renderSubsectionComponent();
const visibilityTab = getByRole('tab', { name: messages.visibilityTabTitle.defaultMessage });
userEvent.click(visibilityTab);
expect(getByText('Subsection visibility')).toBeInTheDocument();
expect(getByText(messages.showEntireSubsection.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.showEntireSubsectionDescription.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.hideContentAfterDue.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.hideContentAfterDueDescription.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.hideEntireSubsection.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.hideEntireSubsectionDescription.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.assessmentResultsVisibility.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.alwaysShowAssessmentResults.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.alwaysShowAssessmentResultsDescription.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.neverShowAssessmentResults.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.neverShowAssessmentResultsDescription.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.showAssessmentResultsPastDue.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.showAssessmentResultsPastDueDescription.defaultMessage)).toBeInTheDocument();
});
it('switches to the subsection Advanced tab and renders correctly', () => {
const { getByRole, getByText } = renderSubsectionComponent();
const advancedTab = getByRole('tab', { name: messages.advancedTabTitle.defaultMessage });
userEvent.click(advancedTab);
expect(getByText(messages.setSpecialExam.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.none.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.timed.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.timedDescription.defaultMessage)).toBeInTheDocument();
});
});
const renderUnitComponent = (props) => render(
<AppProvider store={store}>
<IntlProvider locale="en">
<ConfigureModal
isOpen
onClose={onCloseMock}
onConfigureSubmit={onConfigureSubmitMock}
currentItemData={currentUnitMock}
{...props}
/>
</IntlProvider>,
</AppProvider>,
);
describe('<ConfigureModal /> for Unit', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
it('renders unit ConfigureModal component correctly', () => {
const {
getByText, queryByText, getByRole, getByTestId,
} = renderUnitComponent();
expect(getByText(`${currentUnitMock.displayName} settings`)).toBeInTheDocument();
expect(getByText(messages.unitVisibility.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.hideFromLearners.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.restrictAccessTo.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.unitSelectGroupType.defaultMessage)).toBeInTheDocument();
expect(queryByText(messages.unitSelectGroup.defaultMessage)).not.toBeInTheDocument();
const input = getByTestId('group-type-select');
['0', '1'].forEach(groupeTypeIndex => {
userEvent.selectOptions(input, groupeTypeIndex);
expect(getByText(messages.unitSelectGroup.defaultMessage)).toBeInTheDocument();
currentUnitMock
.userPartitionInfo
.selectablePartitions[groupeTypeIndex].groups
.forEach(g => expect(getByText(g.name)).toBeInTheDocument());
});
expect(getByRole('button', { name: messages.cancelButton.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: messages.saveButton.defaultMessage })).toBeInTheDocument();
});
});
const renderXBlockComponent = (props) => render(
<AppProvider store={store}>
<IntlProvider locale="en">
<ConfigureModal
isOpen
isXBlockComponent
onClose={onCloseMock}
onConfigureSubmit={onConfigureSubmitMock}
currentItemData={currentXBlockMock}
{...props}
/>
</IntlProvider>,
</AppProvider>,
);
describe('<ConfigureModal /> for XBlock', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
it('renders unit ConfigureModal component correctly', () => {
const {
getByText, queryByText, getByRole, getByTestId,
} = renderXBlockComponent();
expect(getByText(`Editing access for: ${currentUnitMock.displayName}`)).toBeInTheDocument();
expect(queryByText(messages.unitVisibility.defaultMessage)).not.toBeInTheDocument();
expect(queryByText(messages.hideFromLearners.defaultMessage)).not.toBeInTheDocument();
expect(getByText(messages.restrictAccessTo.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.unitSelectGroupType.defaultMessage)).toBeInTheDocument();
expect(queryByText(messages.unitSelectGroup.defaultMessage)).not.toBeInTheDocument();
const input = getByTestId('group-type-select');
['0', '1'].forEach(groupeTypeIndex => {
userEvent.selectOptions(input, groupeTypeIndex);
expect(getByText(messages.unitSelectGroup.defaultMessage)).toBeInTheDocument();
currentUnitMock
.userPartitionInfo
.selectablePartitions[groupeTypeIndex].groups
.forEach(g => expect(getByText(g.name)).toBeInTheDocument());
});
expect(getByRole('button', { name: messages.cancelButton.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: messages.saveButton.defaultMessage })).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,117 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Form } from '@openedx/paragon';
import { FormattedMessage, injectIntl, useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
import FormikControl from '../FormikControl';
const PrereqSettings = ({
values,
setFieldValue,
prereqs,
}) => {
const intl = useIntl();
const {
isPrereq,
prereqUsageKey,
prereqMinScore,
prereqMinCompletion,
} = values;
if (isPrereq === null || isPrereq === undefined) {
return null;
}
const handleSelectChange = (e) => {
setFieldValue('prereqUsageKey', e.target.value);
};
const prereqSelectionForm = () => (
<>
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.limitAccessTitle} /></h5>
<hr />
<Form>
<Form.Text><FormattedMessage {...messages.limitAccessDescription} /></Form.Text>
<Form.Group controlId="prereqForm.select">
<Form.Label>
{intl.formatMessage(messages.prerequisiteSelectLabel)}
</Form.Label>
<Form.Control
as="select"
defaultValue={prereqUsageKey}
onChange={handleSelectChange}
role="combobox"
>
<option value="">
{intl.formatMessage(messages.noPrerequisiteOption)}
</option>
{prereqs.map((prereqOption) => (
<option
key={prereqOption.blockUsageKey}
value={prereqOption.blockUsageKey}
>
{prereqOption.blockDisplayName}
</option>
))}
</Form.Control>
</Form.Group>
{prereqUsageKey && (
<>
<FormikControl
name="prereqMinScore"
value={prereqMinScore}
label={<Form.Label>{intl.formatMessage(messages.minScoreLabel)}</Form.Label>}
controlClassName="text-right"
controlClasses="w-7rem"
type="number"
trailingElement="%"
/>
<FormikControl
name="prereqMinCompletion"
value={prereqMinCompletion}
label={<Form.Label>{intl.formatMessage(messages.minCompletionLabel)}</Form.Label>}
controlClassName="text-right"
controlClasses="w-7rem"
type="number"
trailingElement="%"
/>
</>
)}
</Form>
</>
);
const handleCheckboxChange = e => setFieldValue('isPrereq', e.target.checked);
return (
<>
{prereqs.length > 0 && prereqSelectionForm()}
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.prereqTitle} /></h5>
<hr />
<Form.Checkbox checked={isPrereq} onChange={handleCheckboxChange}>
<FormattedMessage {...messages.prereqCheckboxLabel} />
</Form.Checkbox>
</>
);
};
PrereqSettings.defaultProps = {
prereqs: [],
};
PrereqSettings.propTypes = {
values: PropTypes.shape({
isPrereq: PropTypes.bool,
prereqUsageKey: PropTypes.string,
prereqMinScore: PropTypes.number,
prereqMinCompletion: PropTypes.number,
}).isRequired,
prereqs: PropTypes.arrayOf(PropTypes.shape({
blockUsageKey: PropTypes.string.isRequired,
blockDisplayName: PropTypes.string.isRequired,
})),
setFieldValue: PropTypes.func.isRequired,
};
export default injectIntl(PrereqSettings);

View File

@@ -0,0 +1,165 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Form } from '@openedx/paragon';
import {
FormattedMessage, injectIntl, useIntl,
} from '@edx/frontend-platform/i18n';
import { Field } from 'formik';
import classNames from 'classnames';
import messages from './messages';
const UnitTab = ({
isXBlockComponent,
values,
setFieldValue,
showWarning,
userPartitionInfo,
}) => {
const intl = useIntl();
const {
isVisibleToStaffOnly,
selectedPartitionIndex,
selectedGroups,
} = values;
const handleChange = (e) => {
setFieldValue('isVisibleToStaffOnly', e.target.checked);
};
const handleSelect = (e) => {
setFieldValue('selectedPartitionIndex', parseInt(e.target.value, 10));
setFieldValue('selectedGroups', []);
};
const checkIsDeletedGroup = (group) => {
const isGroupSelected = selectedGroups.includes(group.id.toString());
return group.deleted && isGroupSelected;
};
return (
<>
{!isXBlockComponent && (
<>
<h3 className="mt-3"><FormattedMessage {...messages.unitVisibility} /></h3>
<hr />
<Form.Checkbox checked={isVisibleToStaffOnly} onChange={handleChange} data-testid="unit-visibility-checkbox">
<FormattedMessage {...messages.hideFromLearners} />
</Form.Checkbox>
{showWarning && (
<Alert className="mt-2" variant="warning">
<FormattedMessage {...messages.unitVisibilityWarning} />
</Alert>
)}
<hr />
</>
)}
<Form.Group controlId="groupSelect">
<Form.Label as="legend" className="font-weight-bold">
<FormattedMessage {...messages.restrictAccessTo} />
</Form.Label>
<Form.Control
as="select"
name="groupSelect"
value={selectedPartitionIndex}
onChange={handleSelect}
data-testid="group-type-select"
>
<option value="-1" key="-1">
{userPartitionInfo.selectedPartitionIndex === -1
? intl.formatMessage(messages.unitSelectGroupType)
: intl.formatMessage(messages.unitAllLearnersAndStaff)}
</option>
{userPartitionInfo.selectablePartitions.map((partition, index) => (
<option
key={partition.id}
value={index}
>
{partition.name}
</option>
))}
</Form.Control>
{selectedPartitionIndex >= 0 && userPartitionInfo.selectablePartitions.length && (
<Form.Group controlId="select-groups-checkboxes">
<Form.Label><FormattedMessage {...messages.unitSelectGroup} /></Form.Label>
<div
role="group"
className="d-flex flex-column"
data-testid="group-checkboxes"
aria-labelledby="select-groups-checkboxes"
>
{userPartitionInfo.selectablePartitions[selectedPartitionIndex].groups.map((group) => (
<Form.Group
key={group.id}
className="pgn__form-checkbox"
>
<Field
as={Form.Control}
className="flex-grow-0 mr-1"
controlClassName="pgn__form-checkbox-input mr-1"
type="checkbox"
value={`${group.id}`}
name="selectedGroups"
/>
<div>
<Form.Label
className={classNames({ 'text-danger': checkIsDeletedGroup(group) })}
isInline
>
{group.name}
</Form.Label>
{group.deleted && (
<Form.Control.Feedback type="invalid" hasIcon={false}>
{intl.formatMessage(messages.unitSelectDeletedGroupErrorMessage)}
</Form.Control.Feedback>
)}
</div>
</Form.Group>
))}
</div>
</Form.Group>
)}
</Form.Group>
</>
);
};
UnitTab.defaultProps = {
isXBlockComponent: false,
};
UnitTab.propTypes = {
isXBlockComponent: PropTypes.bool,
values: PropTypes.shape({
isVisibleToStaffOnly: PropTypes.bool.isRequired,
selectedPartitionIndex: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
selectedGroups: PropTypes.oneOfType([
PropTypes.string,
]),
}).isRequired,
setFieldValue: PropTypes.func.isRequired,
showWarning: PropTypes.bool.isRequired,
userPartitionInfo: PropTypes.shape({
selectablePartitions: PropTypes.arrayOf(PropTypes.shape({
groups: PropTypes.arrayOf(PropTypes.shape({
deleted: PropTypes.bool.isRequired,
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
selected: PropTypes.bool.isRequired,
}).isRequired).isRequired,
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
scheme: PropTypes.string.isRequired,
}).isRequired).isRequired,
selectedGroupsLabel: PropTypes.string,
selectedPartitionIndex: PropTypes.number.isRequired,
}).isRequired,
};
export default injectIntl(UnitTab);

View File

@@ -0,0 +1,135 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Form } from '@openedx/paragon';
import { FormattedMessage, injectIntl, useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
import { COURSE_BLOCK_NAMES } from '../../constants';
const VisibilityTab = ({
values,
setFieldValue,
category,
showWarning,
isSubsection,
}) => {
const intl = useIntl();
const visibilityTitle = COURSE_BLOCK_NAMES[category]?.name;
const {
isVisibleToStaffOnly,
hideAfterDue,
showCorrectness,
} = values;
const handleChange = (e) => {
setFieldValue('isVisibleToStaffOnly', e.target.checked);
};
const getVisibilityValue = () => {
if (isVisibleToStaffOnly) {
return 'hide';
}
if (hideAfterDue) {
return 'hideDue';
}
return 'show';
};
const visibilityChanged = (e) => {
const selected = e.target.value;
if (selected === 'hide') {
setFieldValue('isVisibleToStaffOnly', true);
setFieldValue('hideAfterDue', false);
} else if (selected === 'hideDue') {
setFieldValue('isVisibleToStaffOnly', false);
setFieldValue('hideAfterDue', true);
} else {
setFieldValue('isVisibleToStaffOnly', false);
setFieldValue('hideAfterDue', false);
}
};
const correctnessChanged = (e) => {
setFieldValue('showCorrectness', e.target.value);
};
return (
<>
<h5 className="mt-4 text-gray-700">
{intl.formatMessage(messages.visibilitySectionTitle, { visibilityTitle })}
</h5>
<hr />
{
isSubsection ? (
<>
<Form.RadioSet
name="subsectionVisibility"
onChange={visibilityChanged}
value={getVisibilityValue()}
>
<Form.Radio value="show">
<FormattedMessage {...messages.showEntireSubsection} />
</Form.Radio>
<Form.Text><FormattedMessage {...messages.showEntireSubsectionDescription} /></Form.Text>
<Form.Radio value="hideDue">
<FormattedMessage {...messages.hideContentAfterDue} />
</Form.Radio>
<Form.Text><FormattedMessage {...messages.hideContentAfterDueDescription} /></Form.Text>
<Form.Radio value="hide">
<FormattedMessage {...messages.hideEntireSubsection} />
</Form.Radio>
<Form.Text><FormattedMessage {...messages.hideEntireSubsectionDescription} /></Form.Text>
</Form.RadioSet>
{showWarning && (
<Alert className="mt-2" variant="warning">
<FormattedMessage {...messages.subsectionVisibilityWarning} />
</Alert>
)}
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.assessmentResultsVisibility} /></h5>
<Form.RadioSet
name="assessmentResultsVisibility"
onChange={correctnessChanged}
value={showCorrectness}
>
<Form.Radio value="always">
<FormattedMessage {...messages.alwaysShowAssessmentResults} />
</Form.Radio>
<Form.Text><FormattedMessage {...messages.alwaysShowAssessmentResultsDescription} /></Form.Text>
<Form.Radio value="never">
<FormattedMessage {...messages.neverShowAssessmentResults} />
</Form.Radio>
<Form.Text><FormattedMessage {...messages.neverShowAssessmentResultsDescription} /></Form.Text>
<Form.Radio value="past_due">
<FormattedMessage {...messages.showAssessmentResultsPastDue} />
</Form.Radio>
<Form.Text><FormattedMessage {...messages.showAssessmentResultsPastDueDescription} /></Form.Text>
</Form.RadioSet>
</>
) : (
<Form.Checkbox checked={isVisibleToStaffOnly} onChange={handleChange} data-testid="visibility-checkbox">
<FormattedMessage {...messages.hideFromLearners} />
</Form.Checkbox>
)
}
{showWarning && !isSubsection && (
<Alert className="mt-2" variant="warning">
<FormattedMessage {...messages.sectionVisibilityWarning} />
</Alert>
)}
</>
);
};
VisibilityTab.propTypes = {
values: PropTypes.shape({
isVisibleToStaffOnly: PropTypes.bool.isRequired,
hideAfterDue: PropTypes.bool.isRequired,
showCorrectness: PropTypes.string.isRequired,
}).isRequired,
setFieldValue: PropTypes.func.isRequired,
category: PropTypes.string.isRequired,
showWarning: PropTypes.bool.isRequired,
isSubsection: PropTypes.bool.isRequired,
};
export default injectIntl(VisibilityTab);

View File

@@ -0,0 +1,199 @@
export const currentSectionMock = {
displayName: 'Section1',
category: 'chapter',
start: '2025-08-10T10:00:00Z',
visibilityState: true,
format: 'Not Graded',
childInfo: {
displayName: 'Subsection',
children: [
{
displayName: 'Subsection 1',
id: 1,
category: 'sequential',
due: '',
start: '2025-08-10T10:00:00Z',
visibilityState: true,
defaultTimeLimitMinutes: null,
hideAfterDue: false,
showCorrectness: false,
format: 'Homework',
courseGraders: ['Homework', 'Exam'],
childInfo: {
displayName: 'Unit',
children: [
{
id: 11,
displayName: 'Subsection_1 Unit 1',
},
],
},
},
{
displayName: 'Subsection 2',
id: 2,
category: 'sequential',
due: '',
start: '2025-08-10T10:00:00Z',
visibilityState: true,
defaultTimeLimitMinutes: null,
hideAfterDue: false,
showCorrectness: false,
format: 'Homework',
courseGraders: ['Homework', 'Exam'],
childInfo: {
displayName: 'Unit',
children: [
{
id: 21,
displayName: 'Subsection_2 Unit 1',
},
],
},
},
{
displayName: 'Subsection 3',
id: 3,
category: 'sequential',
due: '',
start: '2025-08-10T10:00:00Z',
visibilityState: true,
defaultTimeLimitMinutes: null,
hideAfterDue: false,
showCorrectness: false,
format: 'Homework',
courseGraders: ['Homework', 'Exam'],
childInfo: {
children: [],
},
},
],
},
};
export const currentSubsectionMock = {
displayName: 'Subsection 1',
id: 1,
category: 'sequential',
due: '',
start: '2025-08-10T10:00:00Z',
visibilityState: true,
defaultTimeLimitMinutes: null,
hideAfterDue: false,
showCorrectness: false,
format: 'Homework',
courseGraders: ['Homework', 'Exam'],
childInfo: {
displayName: 'Unit',
children: [
{
id: 11,
displayName: 'Subsection_1 Unit 1',
},
{
id: 12,
displayName: 'Subsection_1 Unit 2',
},
],
},
};
export const currentUnitMock = {
displayName: 'Unit 1',
id: 1,
category: 'vertical',
due: '',
start: '2025-08-10T10:00:00Z',
visibilityState: true,
defaultTimeLimitMinutes: null,
hideAfterDue: false,
showCorrectness: false,
userPartitionInfo: {
selectablePartitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 6,
name: 'Honor',
selected: false,
deleted: false,
},
{
id: 2,
name: 'Verified',
selected: false,
deleted: false,
},
],
},
{
id: 1508065533,
name: 'Content Groups',
scheme: 'cohort',
groups: [
{
id: 1224170703,
name: 'Content Group 1',
selected: false,
deleted: false,
},
],
},
],
selectedPartitionIndex: -1,
selectedGroupsLabel: '',
},
};
export const currentXBlockMock = {
displayName: 'Unit 1',
id: 1,
category: 'component',
due: '',
start: '2025-08-10T10:00:00Z',
visibilityState: true,
defaultTimeLimitMinutes: null,
hideAfterDue: false,
showCorrectness: false,
userPartitionInfo: {
selectablePartitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 6,
name: 'Honor',
selected: false,
deleted: false,
},
{
id: 2,
name: 'Verified',
selected: false,
deleted: false,
},
],
},
{
id: 1508065533,
name: 'Content Groups',
scheme: 'cohort',
groups: [
{
id: 1224170703,
name: 'Content Group 1',
selected: false,
deleted: false,
},
],
},
],
selectedPartitionIndex: -1,
selectedGroupsLabel: '',
},
};

View File

@@ -0,0 +1,280 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
title: {
id: 'course-authoring.course-outline.configure-modal.title',
defaultMessage: '{title} settings',
},
componentTitle: {
id: 'course-authoring.course-outline.configure-modal.component.title',
defaultMessage: 'Editing access for: {title}',
description: 'The visibility modal title for unit',
},
basicTabTitle: {
id: 'course-authoring.course-outline.configure-modal.basic-tab.title',
defaultMessage: 'Basic',
},
notGradedTypeOption: {
id: 'course-authoring.course-outline.configure-modal.basic-tab.notGradedTypeOption',
defaultMessage: 'Not Graded',
},
releaseDateAndTime: {
id: 'course-authoring.course-outline.configure-modal.basic-tab.release-date-and-time',
defaultMessage: 'Release date and time',
},
releaseDate: {
id: 'course-authoring.course-outline.configure-modal.basic-tab.release-date',
defaultMessage: 'Release date:',
},
releaseTimeUTC: {
id: 'course-authoring.course-outline.configure-modal.basic-tab.release-time-UTC',
defaultMessage: 'Release time in UTC:',
},
visibilityTabTitle: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.title',
defaultMessage: 'Visibility',
},
visibilitySectionTitle: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.section-visibility',
defaultMessage: '{visibilityTitle} visibility',
},
unitVisibility: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.unit-visibility',
defaultMessage: 'Unit visibility',
},
hideFromLearners: {
id: 'course-authoring.course-outline.configure-modal.visibility.hide-from-learners',
defaultMessage: 'Hide from learners',
},
restrictAccessTo: {
id: 'course-authoring.course-outline.configure-modal.visibility.restrict-access-to',
defaultMessage: 'Restrict access to',
},
sectionVisibilityWarning: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.section-visibility-warning',
defaultMessage: 'If you make this section visible to learners, learners will be able to see its content after the release date has passed and you have published the section. Only units that are explicitly hidden from learners will remain hidden after you clear this option for the section.',
},
unitVisibilityWarning: {
id: 'course-authoring.course-outline.configure-modal.unit-tab.unit-visibility-warning',
defaultMessage: 'If the unit was previously published and released to learners, any changes you made to the unit when it was hidden will now be visible to learners.',
},
subsectionVisibilityWarning: {
id: 'course-authoring.course-outline.configure-modal.unit-tab.subsection-visibility-warning',
defaultMessage: 'If you select an option other than "Hide entire subsection", published units in this subsection will become available to learners unless they are explicitly hidden.',
},
unitSelectGroup: {
id: 'course-authoring.course-outline.configure-modal.unit-tab.unit-select-group',
defaultMessage: 'Select one or more groups:',
},
unitSelectGroupType: {
id: 'course-authoring.course-outline.configure-modal.unit-tab.unit-select-group-type',
defaultMessage: 'Select a group type',
},
unitSelectDeletedGroupErrorMessage: {
id: 'course-authoring.course-outline.configure-modal.unit-tab.unit-select-group-deleted-error-message',
defaultMessage: 'This group no longer exists. Choose another group or remove the access restriction.',
description: 'The alert text of no longer available group',
},
unitAllLearnersAndStaff: {
id: 'course-authoring.course-outline.configure-modal.unit-tab.unit-all-learners-staff',
defaultMessage: 'All Learners and Staff',
},
cancelButton: {
id: 'course-authoring.course-outline.configure-modal.button.cancel',
defaultMessage: 'Cancel',
},
saveButton: {
id: 'course-authoring.course-outline.configure-modal.button.label',
defaultMessage: 'Save',
},
grading: {
id: 'course-authoring.course-outline.configure-modal.basic-tab.grading',
defaultMessage: 'Grading',
},
gradeAs: {
id: 'course-authoring.course-outline.configure-modal.basic-tab.grade-as',
defaultMessage: 'Grade as:',
},
dueDate: {
id: 'course-authoring.course-outline.configure-modal.basic-tab.due-date',
defaultMessage: 'Due date:',
},
dueTimeUTC: {
id: 'course-authoring.course-outline.configure-modal.basic-tab.due-time-UTC',
defaultMessage: 'Due time in UTC:',
},
subsectionVisibility: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.subsection-visibility',
defaultMessage: 'Subsection visibility',
},
showEntireSubsection: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.show-entire-subsection',
defaultMessage: 'Show entire subsection',
},
showEntireSubsectionDescription: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.show-entire-subsection-description',
defaultMessage: 'Learners see the published subsection and can access its content',
},
hideContentAfterDue: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.hide-content-after-due',
defaultMessage: 'Hide content after due date',
},
hideContentAfterDueDescription: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.hide-content-after-due-description',
defaultMessage: 'After the subsection\'s due date has passed, learners can no longer access its content. The subsection is not included in grade calculations.',
},
hideEntireSubsection: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.hide-entire-subsection',
defaultMessage: 'Hide entire subsection',
},
hideEntireSubsectionDescription: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.hide-entire-subsection-description',
defaultMessage: 'Learners do not see the subsection in the course outline. The subsection is not included in grade calculations.',
},
assessmentResultsVisibility: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.assessment-results-visibility',
defaultMessage: 'Assessment Results Visibility',
},
alwaysShowAssessmentResults: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.always-show-assessment-results',
defaultMessage: 'Always show assessment results',
},
alwaysShowAssessmentResultsDescription: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.always-show-assessment-results-description',
defaultMessage: 'When learners submit an answer to an assessment, they immediately see whether the answer is correct or incorrect, and the score received.',
},
neverShowAssessmentResults: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.never-show-assessment-results',
defaultMessage: 'Never show assessment results',
},
neverShowAssessmentResultsDescription: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.never-show-assessment-results-description',
defaultMessage: 'Learners never see whether their answers to assessments are correct or incorrect, nor the score received.',
},
showAssessmentResultsPastDue: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.show-assessment-results-past-due',
defaultMessage: 'Show assessment results when subsection is past due',
},
showAssessmentResultsPastDueDescription: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.show-assessment-results-past-due-description',
defaultMessage: 'Learners do not see whether their answer to assessments were correct or incorrect, nor the score received, until after the due date for the subsection has passed. If the subsection does not have a due date, learners always see their scores when they submit answers to assessments.',
},
setSpecialExam: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.set-special-exam',
defaultMessage: 'Set as a special exam',
},
none: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.none',
defaultMessage: 'None',
},
timed: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.timed',
defaultMessage: 'Timed',
},
timedDescription: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.timed-description',
defaultMessage: 'Use a timed exam to limit the time learners can spend on problems in this subsection. Learners must submit answers before the time expires. You can allow additional time for individual learners through the instructor Dashboard.',
},
proctoredExam: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.proctoredExam',
defaultMessage: 'Proctored',
},
proctoredExamDescription: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.timed-description',
defaultMessage: 'Proctored exams are timed and they record video of each learner taking the exam. The videos are then reviewed to ensure that learners follow all examination rules. Please note that setting this exam as proctored will change the visibility settings to "Hide content after due date."',
},
onboardingExam: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.onboardingExam',
defaultMessage: 'Onboarding',
},
onboardingExamDescription: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.timed-description',
defaultMessage: 'Use Onboarding to introduce learners to proctoring, verify their identity, and create an onboarding profile. Learners must complete the onboarding profile step prior to taking a proctored exam. Profile reviews take 2+ business days.',
},
practiceExam: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.practiceExam',
defaultMessage: 'Practice proctored',
},
practiceExamDescription: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.timed-description',
defaultMessage: 'Use a practice proctored exam to introduce learners to the proctoring tools and processes. Results of a practice exam do not affect a learner\'s grade.',
},
advancedTabTitle: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.title',
defaultMessage: 'Advanced',
},
timeAllotted: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.time-allotted',
defaultMessage: 'Time allotted (HH:MM):',
},
timeLimitDescription: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.time-limit-description',
defaultMessage: 'Select a time allotment for the exam. If it is over 24 hours, type in the amount of time. You can grant individual learners extra time to complete the exam through the Instructor Dashboard.',
},
prereqTitle: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.prereqTitle',
defaultMessage: 'Use as a Prerequisite',
},
prereqCheckboxLabel: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.prereqCheckboxLabel',
defaultMessage: 'Make this subsection available as a prerequisite to other content',
},
limitAccessTitle: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.limitAccessTitle',
defaultMessage: 'Limit access',
},
limitAccessDescription: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.limitAccessDescription',
defaultMessage: 'Select a prerequisite subsection and enter a minimum score percentage and minimum completion percentage to limit access to this subsection. Allowed values are 0-100',
},
noPrerequisiteOption: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.noPrerequisiteOption',
defaultMessage: 'No prerequisite',
},
prerequisiteSelectLabel: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.prerequisiteSelectLabel',
defaultMessage: 'Prerequisite:',
},
minScoreLabel: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.minScoreLabel',
defaultMessage: 'Minimum score:',
},
minCompletionLabel: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.minCompletionLabel',
defaultMessage: 'Minimum completion:',
},
minScoreError: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.minScoreError',
defaultMessage: 'The minimum score percentage must be a whole number between 0 and 100.',
},
minCompletionError: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.minCompletionError',
defaultMessage: 'The minimum completion percentage must be a whole number between 0 and 100.',
},
proctoredExamLockedAndisNotProctoredExamAlert: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.proctoredExamLockedAndisNotProctoredExamAlert',
defaultMessage: 'This subsection was released to learners as a proctored exam, but was reverted back to a basic or timed exam. You may not configure it as a proctored exam now. Contact edX Support for assistance.',
},
proctoredExamLockedAndisProctoredExamAlert: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.proctoredExamLockedAndisProctoredExamAlert',
defaultMessage: 'This proctored exam has been released to learners. You may not convert it to another type of special exam. You may revert this subsection back to being a basic exam by selecting \'None\', or a timed exam, but you will NOT be able to configure it as a proctored exam in the future.',
},
reviewRulesLabel: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.reviewRulesLabel',
defaultMessage: 'Review rules',
},
reviewRulesDescription: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.reviewRulesDescription',
defaultMessage: 'Specify any rules or rule exceptions that the proctoring review team should enforce when reviewing the videos. For example, you could specify that calculators are allowed. These specified rules are visible to learners before the learners start the exam.',
},
reviewRulesDescriptionWithLink: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.reviewRulesDescriptionWithLink',
defaultMessage: 'Specify any rules or rule exceptions that the proctoring review team should enforce when reviewing the videos. For example, you could specify that calculators are allowed. These specified rules are visible to learners before the learners start the exam, along with the {hyperlink}.',
},
reviewRulesDescriptionLinkText: {
id: 'course-authoring.course-outline.configure-modal.advanced-tab.reviewRulesDescriptionLinkText',
defaultMessage: 'general proctored exam rules',
},
});
export default messages;

View File

@@ -8,3 +8,4 @@
@import "./course-stepper/CouseStepper";
@import "./tag-count/TagCount";
@import "./modal-dropzone/ModalDropzone";
@import "./configure-modal/ConfigureModal";