feat: Add permissions checks for group_configuration, grading, outline (#829)

* feat: update header options for access control, course outline access checks, grade-settings access checks, and view only for grading page
This commit is contained in:
hilary sinkoff
2024-02-15 14:52:18 -06:00
committed by hsinkoff
parent 1e9146a5b9
commit c754a5e519
16 changed files with 289 additions and 27 deletions

View File

@@ -41,6 +41,9 @@ import DeleteModal from './delete-modal/DeleteModal';
import PageAlerts from './page-alerts/PageAlerts';
import { useCourseOutline } from './hooks';
import messages from './messages';
import { useUserPermissions } from '../generic/hooks';
import { getUserPermissionsEnabled } from '../generic/data/selectors';
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
const CourseOutline = ({ courseId }) => {
const intl = useIntl();
@@ -108,6 +111,12 @@ const CourseOutline = ({ courseId }) => {
const [sections, setSections] = useState(sectionsList);
const { checkPermission } = useUserPermissions();
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
const hasOutlinePermissions = !userPermissionsEnabled || (
userPermissionsEnabled && (checkPermission('manage_libraries') || checkPermission('manage_content'))
);
let initialSections = [...sectionsList];
const {
@@ -241,6 +250,12 @@ const CourseOutline = ({ courseId }) => {
setSections(sectionsList);
}, [sectionsList]);
if (!hasOutlinePermissions) {
return (
<PermissionDeniedAlert />
);
}
if (isLoading) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return (

View File

@@ -48,10 +48,15 @@ import pasteButtonMessages from './paste-button/messages';
import subsectionMessages from './subsection-card/messages';
import pageAlertMessages from './page-alerts/messages';
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api';
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
let axiosMock;
let store;
const mockPathname = '/foo-bar';
const courseId = '123';
const userId = 3;
const userPermissionsData = { permissions: [] };
window.HTMLElement.prototype.scrollIntoView = jest.fn();
@@ -90,7 +95,7 @@ describe('<CourseOutline />', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
userId,
username: 'abc123',
administrator: true,
roles: [],
@@ -102,6 +107,14 @@ describe('<CourseOutline />', () => {
axiosMock
.onGet(getCourseOutlineIndexApiUrl(courseId))
.reply(200, courseOutlineIndexMock);
axiosMock
.onGet(getUserPermissionsEnabledFlagUrl)
.reply(200, { enabled: false });
axiosMock
.onGet(getUserPermissionsUrl(courseId, userId))
.reply(200, userPermissionsData);
executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch);
});
@@ -114,6 +127,13 @@ describe('<CourseOutline />', () => {
});
});
it('should render permissionDenied if incorrect permissions', async () => {
const { getByTestId } = render(<RootWrapper />);
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
expect(getByTestId('permissionDeniedAlert')).toBeVisible();
});
it('check reindex and render success alert is correctly', async () => {
const { findByText, findByTestId } = render(<RootWrapper />);

View File

@@ -30,6 +30,9 @@ import CreditSection from './credit-section';
import DeadlineSection from './deadline-section';
import { useConvertGradeCutoffs, useUpdateGradingData } from './hooks';
import getPageHeadTitle from '../generic/utils';
import { useUserPermissions } from '../generic/hooks';
import { getUserPermissionsEnabled } from '../generic/data/selectors';
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
const GradingSettings = ({ intl, courseId }) => {
const gradingSettingsData = useSelector(getGradingSettings);
@@ -43,6 +46,14 @@ const GradingSettings = ({ intl, courseId }) => {
const [isQueryPending, setIsQueryPending] = useState(false);
const [showOverrideInternetConnectionAlert, setOverrideInternetConnectionAlert] = useState(false);
const [eligibleGrade, setEligibleGrade] = useState(null);
const { checkPermission } = useUserPermissions();
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
const hasGradingPermissions = !userPermissionsEnabled || (
userPermissionsEnabled && (checkPermission('manage_course_settings') || checkPermission('view_course_settings'))
);
const viewOnly = !userPermissionsEnabled || (
userPermissionsEnabled && checkPermission('view_course_settings') && !checkPermission('manage_course_settings')
);
const courseDetails = useModel('courseDetails', courseId);
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
@@ -83,6 +94,12 @@ const GradingSettings = ({ intl, courseId }) => {
dispatch(fetchCourseSettingsQuery(courseId));
}, [courseId]);
if (!hasGradingPermissions) {
return (
<PermissionDeniedAlert />
);
}
if (isLoading) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
@@ -156,6 +173,7 @@ const GradingSettings = ({ intl, courseId }) => {
resetDataRef={resetDataRef}
setOverrideInternetConnectionAlert={setOverrideInternetConnectionAlert}
setEligibleGrade={setEligibleGrade}
viewOnly={viewOnly}
/>
</section>
{courseSettingsData.creditEligibilityEnabled && courseSettingsData.isCreditCourse && (
@@ -170,6 +188,7 @@ const GradingSettings = ({ intl, courseId }) => {
minimumGradeCredit={minimumGradeCredit}
setGradingData={setGradingData}
setShowSuccessAlert={setShowSuccessAlert}
viewOnly={viewOnly}
/>
</section>
)}
@@ -183,6 +202,7 @@ const GradingSettings = ({ intl, courseId }) => {
gracePeriod={gracePeriod}
setGradingData={setGradingData}
setShowSuccessAlert={setShowSuccessAlert}
viewOnly={viewOnly}
/>
</section>
<section>
@@ -201,11 +221,13 @@ const GradingSettings = ({ intl, courseId }) => {
setGradingData={setGradingData}
courseAssignmentLists={courseAssignmentLists}
setShowSuccessAlert={setShowSuccessAlert}
viewOnly={viewOnly}
/>
<Button
variant="primary"
iconBefore={IconAdd}
onClick={handleAddAssignment}
disabled={viewOnly}
>
{intl.formatMessage(messages.addNewAssignmentTypeBtn)}
</Button>

View File

@@ -12,9 +12,15 @@ import gradingSettings from './__mocks__/gradingSettings';
import GradingSettings from './GradingSettings';
import messages from './messages';
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api';
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
import { executeThunk } from '../utils';
const courseId = '123';
const userId = 3;
let axiosMock;
let store;
const userPermissionsData = { permissions: [] };
const RootWrapper = () => (
<AppProvider store={store}>
@@ -28,7 +34,7 @@ describe('<GradingSettings />', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
userId,
username: 'abc123',
administrator: true,
roles: [],
@@ -40,6 +46,25 @@ describe('<GradingSettings />', () => {
axiosMock
.onGet(getGradingSettingsApiUrl(courseId))
.reply(200, gradingSettings);
axiosMock
.onGet(getUserPermissionsEnabledFlagUrl)
.reply(200, { enabled: false });
axiosMock
.onGet(getUserPermissionsUrl(courseId, userId))
.reply(200, userPermissionsData);
executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
});
afterEach(() => {
axiosMock
.onGet(getUserPermissionsEnabledFlagUrl)
.reply(200, { enabled: false });
axiosMock
.onGet(getUserPermissionsUrl(courseId, userId))
.reply(200, userPermissionsData);
executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
});
it('should render without errors', async () => {
@@ -54,6 +79,38 @@ describe('<GradingSettings />', () => {
});
});
it('should render without errors if access controlled by permissions', async () => {
const { getByText, getAllByText, getAllByTestId } = render(<RootWrapper />);
axiosMock
.onGet(getUserPermissionsEnabledFlagUrl)
.reply(200, { enabled: true });
const permissionsData = { permissions: ['manage_course_settings'] };
axiosMock
.onGet(getUserPermissionsUrl(courseId, userId))
.reply(200, permissionsData);
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
await waitFor(() => {
const gradingElements = getAllByText(messages.headingTitle.defaultMessage);
const gradingTitle = gradingElements[0];
const segmentButton = getAllByTestId('grading-scale-btn-add-segment');
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
expect(gradingTitle).toBeInTheDocument();
expect(getByText(messages.policy.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.policiesDescription.defaultMessage)).toBeInTheDocument();
expect(segmentButton.length).toEqual(1);
expect(segmentButton[0]).toBeInTheDocument();
expect(segmentButton[0].disabled).toEqual(false);
});
});
it('should render permissionDenied if incorrect permissions', async () => {
const { getByTestId } = render(<RootWrapper />);
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
expect(getByTestId('permissionDeniedAlert')).toBeVisible();
});
it('should update segment input value and show save alert', async () => {
const { getByTestId, getAllByTestId } = render(<RootWrapper />);
await waitFor(() => {
@@ -76,6 +133,7 @@ describe('<GradingSettings />', () => {
expect(segmentInput).toHaveValue('a');
});
});
it('should save segment input changes and display saving message', async () => {
const { getByText, getAllByTestId } = render(<RootWrapper />);
await waitFor(() => {
@@ -88,4 +146,23 @@ describe('<GradingSettings />', () => {
expect(getByText(messages.buttonSavingText.defaultMessage)).toBeInTheDocument();
});
});
it('should show disabled button to update segments', async () => {
const { getAllByTestId } = render(<RootWrapper />);
axiosMock
.onGet(getUserPermissionsEnabledFlagUrl)
.reply(200, { enabled: true });
const permissionsData = { permissions: ['view_course_settings'] };
axiosMock
.onGet(getUserPermissionsUrl(courseId, userId))
.reply(200, permissionsData);
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
await waitFor(() => {
const segmentButton = getAllByTestId('grading-scale-btn-add-segment');
expect(segmentButton.length).toEqual(1);
expect(segmentButton[0]).toBeInTheDocument();
expect(segmentButton[0].disabled).toEqual(true);
});
});
});

View File

@@ -20,6 +20,7 @@ const AssignmentItem = ({
secondErrorMsg,
gradeField,
trailingElement,
viewOnly,
}) => (
<li className={className}>
<Form.Group className={classNames('form-group-custom', {
@@ -37,6 +38,7 @@ const AssignmentItem = ({
value={value}
isInvalid={errorEffort}
trailingElement={trailingElement}
disabled={viewOnly}
/>
<Form.Control.Feedback className="grading-description">
{descriptions}
@@ -81,6 +83,7 @@ AssignmentItem.propTypes = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
gradeField: PropTypes.shape(defaultAssignmentsPropTypes),
trailingElement: PropTypes.string,
viewOnly: PropTypes.bool.isRequired,
};
export default AssignmentItem;

View File

@@ -8,7 +8,7 @@ import { ASSIGNMENT_TYPES, DUPLICATE_ASSIGNMENT_NAME } from '../utils/enum';
import messages from '../messages';
const AssignmentTypeName = ({
intl, value, errorEffort, onChange,
intl, value, errorEffort, onChange, viewOnly,
}) => {
const initialAssignmentName = useRef(value);
@@ -28,6 +28,7 @@ const AssignmentTypeName = ({
onChange={onChange}
value={value}
isInvalid={Boolean(errorEffort)}
disabled={viewOnly}
/>
<Form.Control.Feedback className="grading-description">
{intl.formatMessage(messages.assignmentTypeNameDescription)}
@@ -64,6 +65,7 @@ AssignmentTypeName.propTypes = {
onChange: PropTypes.func.isRequired,
errorEffort: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired,
viewOnly: PropTypes.bool.isRequired,
};
export default injectIntl(AssignmentTypeName);

View File

@@ -22,6 +22,7 @@ const AssignmentSection = ({
setGradingData,
courseAssignmentLists,
setShowSuccessAlert,
viewOnly,
}) => {
const [errorList, setErrorList] = useState({});
const {
@@ -83,6 +84,7 @@ const AssignmentSection = ({
value={gradeField.type}
errorEffort={errorList[`${type}-${gradeField.id}`]}
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
viewOnly={viewOnly}
/>
<AssignmentItem
className="course-grading-assignment-abbreviation"
@@ -92,6 +94,7 @@ const AssignmentSection = ({
name="shortLabel"
value={gradeField.shortLabel}
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
viewOnly={viewOnly}
/>
<AssignmentItem
className="course-grading-assignment-total-grade"
@@ -106,6 +109,7 @@ const AssignmentSection = ({
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
errorEffort={errorList[`${weight}-${gradeField.id}`]}
trailingElement="%"
viewOnly={viewOnly}
/>
<AssignmentItem
className="course-grading-assignment-total-number"
@@ -118,6 +122,7 @@ const AssignmentSection = ({
value={gradeField.minCount}
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
errorEffort={errorList[`${minCount}-${gradeField.id}`]}
viewOnly={viewOnly}
/>
<AssignmentItem
className="course-grading-assignment-number-droppable"
@@ -134,6 +139,7 @@ const AssignmentSection = ({
type: gradeField.type,
})}
errorEffort={errorList[`${dropCount}-${gradeField.id}`]}
viewOnly={viewOnly}
/>
</ol>
{showDefinedCaseAlert && (
@@ -185,6 +191,7 @@ const AssignmentSection = ({
variant="outline-primary"
size="sm"
onClick={() => handleRemoveAssignment(gradeField.id)}
disabled={viewOnly}
>
{intl.formatMessage(messages.assignmentDeleteButton)}
</Button>
@@ -210,6 +217,7 @@ AssignmentSection.propTypes = {
graders: PropTypes.arrayOf(
PropTypes.shape(defaultAssignmentsPropTypes),
),
viewOnly: PropTypes.bool.isRequired,
};
export default injectIntl(AssignmentSection);

View File

@@ -18,6 +18,7 @@ const RootWrapper = (props = {}) => (
minimumGradeCredit={0.1}
setGradingData={jest.fn()}
setShowSuccessAlert={jest.fn()}
viewOnly={false}
{...props}
/>
</IntlProvider>
@@ -38,6 +39,14 @@ describe('<CreditSection />', () => {
expect(inputElement.value).toBe('10');
fireEvent.change(inputElement, { target: { value: '2' } });
expect(testObj.minimumGradeCredit).toBe(0.02);
expect(inputElement.disabled).toBe(false);
});
});
it('should disable the fields if viewOnly', async () => {
const { getByTestId } = render(<RootWrapper viewOnly />);
await waitFor(() => {
const inputElement = getByTestId('minimum-grade-credit-input');
expect(inputElement.disabled).toBe(true);
});
});
});

View File

@@ -7,7 +7,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages';
const CreditSection = ({
intl, eligibleGrade, setShowSavePrompt, minimumGradeCredit, setGradingData, setShowSuccessAlert,
intl, eligibleGrade, setShowSavePrompt, minimumGradeCredit, setGradingData, setShowSuccessAlert, viewOnly,
}) => {
const [errorEffort, setErrorEffort] = useState(false);
@@ -46,6 +46,7 @@ const CreditSection = ({
value={Math.round(parseFloat(minimumGradeCredit) * 100) || ''}
name="minimum_grade_credit"
onChange={handleCreditChange}
disabled={viewOnly}
/>
<Form.Control.Feedback className="grading-description">
{intl.formatMessage(messages.creditEligibilityDescription)}
@@ -66,6 +67,7 @@ CreditSection.propTypes = {
setGradingData: PropTypes.func.isRequired,
setShowSuccessAlert: PropTypes.func.isRequired,
minimumGradeCredit: PropTypes.number.isRequired,
viewOnly: PropTypes.bool.isRequired,
};
export default injectIntl(CreditSection);

View File

@@ -22,6 +22,7 @@ const RootWrapper = (props = {}) => (
setShowSavePrompt={jest.fn()}
setGradingData={jest.fn()}
setShowSuccessAlert={jest.fn()}
viewOnly={false}
{...props}
/>
</IntlProvider>
@@ -46,6 +47,7 @@ describe('<DeadlineSection />', () => {
fireEvent.change(inputElement, { target: { value: '13:13' } });
expect(testObj.gracePeriod.hours).toBe(13);
expect(testObj.gracePeriod.minutes).toBe(13);
expect(inputElement.disabled).toEqual(false);
});
});
it('checking deadline input value if grace Period equal null', async () => {
@@ -78,4 +80,11 @@ describe('<DeadlineSection />', () => {
expect(getByText(`Grace period must be specified in ${TIME_FORMAT.toUpperCase()} format.`)).toBeInTheDocument();
});
});
it('checking deadline input is disabled if viewOnly', async () => {
const { getByTestId } = render(<RootWrapper viewOnly />);
await waitFor(() => {
const inputElement = getByTestId('deadline-period-input');
expect(inputElement.disabled).toEqual(true);
});
});
});

View File

@@ -9,7 +9,7 @@ import { formatTime, timerValidation } from './utils';
import messages from './messages';
const DeadlineSection = ({
intl, setShowSavePrompt, gracePeriod, setGradingData, setShowSuccessAlert,
intl, setShowSavePrompt, gracePeriod, setGradingData, setShowSuccessAlert, viewOnly,
}) => {
const timeStampValue = gracePeriod
? gracePeriod.hours && `${formatTime(gracePeriod.hours)}:${formatTime(gracePeriod.minutes)}`
@@ -52,6 +52,7 @@ const DeadlineSection = ({
value={newDeadlineValue}
onChange={handleDeadlineChange}
placeholder={TIME_FORMAT.toUpperCase()}
disabled={viewOnly}
/>
<Form.Control.Feedback className="grading-description">
{intl.formatMessage(messages.gracePeriodOnDeadlineDescription)}
@@ -78,6 +79,7 @@ DeadlineSection.propTypes = {
hours: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
minutes: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}),
viewOnly: PropTypes.bool.isRequired,
};
export default injectIntl(DeadlineSection);

View File

@@ -23,6 +23,7 @@ const GradingScale = ({
sortedGrades,
setOverrideInternetConnectionAlert,
setEligibleGrade,
viewOnly,
}) => {
const [gradingSegments, setGradingSegments] = useState(sortedGrades);
const [letters, setLetters] = useState(gradeLetters);
@@ -191,7 +192,7 @@ const GradingScale = ({
<IconButtonWithTooltip
tooltipPlacement="top"
tooltipContent={intl.formatMessage(messages.addNewSegmentButtonAltText)}
disabled={gradingSegments.length >= 5}
disabled={gradingSegments.length >= 5 || viewOnly}
data-testid="grading-scale-btn-add-segment"
className="mr-3"
src={IconAdd}
@@ -245,6 +246,7 @@ GradingScale.propTypes = {
}),
).isRequired,
setEligibleGrade: PropTypes.func.isRequired,
viewOnly: PropTypes.bool.isRequired,
};
export default injectIntl(GradingScale);

View File

@@ -16,7 +16,7 @@ const sortedGrades = [
{ current: 20, previous: 0 },
];
const RootWrapper = () => (
const RootWrapper = (viewOnly = { viewOnly: false }) => (
<IntlProvider locale="en" messages={{}}>
<GradingScale
intl={injectIntl}
@@ -29,6 +29,7 @@ const RootWrapper = () => (
setGradingData={jest.fn()}
setOverrideInternetConnectionAlert={jest.fn()}
setEligibleGrade={jest.fn()}
{...viewOnly}
/>
</IntlProvider>
);
@@ -73,6 +74,26 @@ describe('<GradingScale />', () => {
});
});
it('should not disable new grading segment button when viewOnly=false', async () => {
const { getAllByTestId } = render(<RootWrapper />);
await waitFor(() => {
const addNewSegmentBtn = getAllByTestId('grading-scale-btn-add-segment');
expect(addNewSegmentBtn).toHaveLength(1);
expect(addNewSegmentBtn[0]).toBeInTheDocument();
expect(addNewSegmentBtn[0].disabled).toBe(false);
});
});
it('should disable new grading segment button when viewOnly', async () => {
const { getAllByTestId } = render(<RootWrapper viewOnly />);
await waitFor(() => {
const addNewSegmentBtn = getAllByTestId('grading-scale-btn-add-segment');
expect(addNewSegmentBtn).toHaveLength(1);
expect(addNewSegmentBtn[0]).toBeInTheDocument();
expect(addNewSegmentBtn[0].disabled).toBe(true);
});
});
it('should remove grading segment when "Remove" button is clicked', async () => {
const { getAllByTestId } = render(<RootWrapper />);
await waitFor(() => {

View File

@@ -25,8 +25,11 @@ const Header = ({
const userPermissions = useSelector(getUserPermissions);
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
const hasContentPermissions = !userPermissionsEnabled || (userPermissionsEnabled && checkPermission('manage_content'));
const hasSettingsPermissions = !userPermissionsEnabled
const hasOutlinePermissions = !userPermissionsEnabled || (userPermissionsEnabled && (checkPermission('manage_content') || checkPermission('manage_libraries')));
const hasAdvancedSettingsAccess = !userPermissionsEnabled
|| (userPermissionsEnabled && (checkPermission('manage_advanced_settings') || checkPermission('view_course_settings')));
const hasSettingsPermissions = !userPermissionsEnabled
|| (userPermissionsEnabled && (checkPermission('manage_course_settings') || checkPermission('view_course_settings')));
const hasToolsPermissions = !userPermissionsEnabled
|| (userPermissionsEnabled && (checkPermission('manage_course_settings') || checkPermission('view_course_settings')));
const studioBaseUrl = getConfig().STUDIO_BASE_URL;
@@ -35,6 +38,7 @@ const Header = ({
courseId,
intl,
hasContentPermissions,
hasOutlinePermissions,
});
const mainMenuDropdowns = [];
const toolsMenu = getToolsMenuItems({
@@ -69,6 +73,7 @@ const Header = ({
courseId,
intl,
hasSettingsPermissions,
hasAdvancedSettingsAccess,
}),
},
);

View File

@@ -7,15 +7,20 @@ export const getContentMenuItems = ({
courseId,
intl,
hasContentPermissions,
hasOutlinePermissions,
}) => {
const items = [];
if (hasContentPermissions) {
if (hasOutlinePermissions) {
items.push(
{
href: `${studioBaseUrl}/course/${courseId}`,
title: intl.formatMessage(messages['header.links.outline']),
},
);
}
if (hasContentPermissions) {
items.push(
{
href: `${studioBaseUrl}/course_info/${courseId}`,
title: intl.formatMessage(messages['header.links.updates']),
@@ -31,11 +36,12 @@ export const getContentMenuItems = ({
);
}
if (getConfig().ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN === 'true') {
items.push({
href: `${studioBaseUrl}/videos/${courseId}`,
title: intl.formatMessage(messages['header.links.videoUploads']),
}
);
items.push(
{
href: `${studioBaseUrl}/videos/${courseId}`,
title: intl.formatMessage(messages['header.links.videoUploads']),
},
);
}
return items;
@@ -45,6 +51,7 @@ export const getSettingMenuItems = ({
studioBaseUrl,
courseId,
intl,
hasAdvancedSettingsAccess,
hasSettingsPermissions,
}) => {
const items = [];
@@ -54,20 +61,30 @@ export const getSettingMenuItems = ({
href: `${studioBaseUrl}/settings/details/${courseId}`,
title: intl.formatMessage(messages['header.links.scheduleAndDetails']),
},
{
href: `${studioBaseUrl}/settings/grading/${courseId}`,
title: intl.formatMessage(messages['header.links.grading']),
},
);
if (hasSettingsPermissions) {
items.push(
{
href: `${studioBaseUrl}/settings/grading/${courseId}`,
title: intl.formatMessage(messages['header.links.grading']),
},
);
}
items.push(
{
href: `${studioBaseUrl}/course_team/${courseId}`,
title: intl.formatMessage(messages['header.links.courseTeam']),
},
{
href: `${studioBaseUrl}/group_configurations/course-v1:${courseId}`,
title: intl.formatMessage(messages['header.links.groupConfigurations']),
},
);
if (hasSettingsPermissions) {
items.push(
{
href: `${studioBaseUrl}/group_configurations/course-v1:${courseId}`,
title: intl.formatMessage(messages['header.links.groupConfigurations']),
},
);
}
if (hasAdvancedSettingsAccess) {
items.push(
{
href: `${studioBaseUrl}/settings/advanced/${courseId}`,

View File

@@ -8,8 +8,8 @@ const baseProps = {
formatMessage: jest.fn(),
},
};
const contentProps = { ...baseProps, hasContentPermissions: true };
const settingProps = { ...baseProps, hasSettingsPermissions: true };
const contentProps = { ...baseProps, hasContentPermissions: true, hasOutlinePermissions: true };
const settingProps = { ...baseProps, hasAdvancedSettingsAccess: true, hasSettingsPermissions: true };
const toolsProps = { ...baseProps, hasToolsPermissions: true };
describe('header utils', () => {
@@ -35,19 +35,67 @@ describe('header utils', () => {
...getConfig(),
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: 'true',
});
const actualItems = getContentMenuItems({ ...baseProps, hasContentPermissions: false });
const actualItems = getContentMenuItems({
...baseProps,
hasContentPermissions: false,
hasOutlinePermissions: false,
});
expect(actualItems).toHaveLength(1);
});
it('should include Outline if outline permissions', () => {
setConfig({
...getConfig(),
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: 'false',
});
const actualItems = getContentMenuItems({
...baseProps,
hasContentPermissions: false,
hasOutlinePermissions: true,
});
expect(actualItems).toHaveLength(1);
});
it('should include content sections if content permissions', () => {
setConfig({
...getConfig(),
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: 'false',
});
const actualItems = getContentMenuItems({
...baseProps,
hasContentPermissions: true,
hasOutlinePermissions: false,
});
expect(actualItems).toHaveLength(3);
});
});
describe('getSettingMenuItems', async () => {
it('should include all options', () => {
const actualItems = getSettingMenuItems(settingProps);
expect(actualItems).toHaveLength(6);
});
it('should not include Advanced Settings option', () => {
const actualItems = getSettingMenuItems({ ...baseProps, hasSettingsPermissions: false });
it('should include Advanced Settings option, but not settings options', () => {
const actualItems = getSettingMenuItems({
...baseProps,
hasSettingsPermissions: false,
hasAdvancedSettingsAccess: true,
});
expect(actualItems).toHaveLength(4);
});
it('should include settings, but not advanced settings', () => {
const actualItems = getSettingMenuItems({
...baseProps,
hasSettingsPermissions: true,
hasAdvancedSettingsAccess: false,
});
expect(actualItems).toHaveLength(5);
});
it('should only include default options', () => {
const actualItems = getSettingMenuItems({
...baseProps,
hasSettingsPermissions: false,
hasAdvancedSettingsAccess: false,
});
expect(actualItems).toHaveLength(3);
});
});
describe('getToolsMenuItems', async () => {
it('should include all options', () => {