diff --git a/src/course-outline/CourseOutline.jsx b/src/course-outline/CourseOutline.jsx index 429d0e410..5ea86cea2 100644 --- a/src/course-outline/CourseOutline.jsx +++ b/src/course-outline/CourseOutline.jsx @@ -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 ( + + ); + } + if (isLoading) { // eslint-disable-next-line react/jsx-no-useless-fragment return ( diff --git a/src/course-outline/CourseOutline.test.jsx b/src/course-outline/CourseOutline.test.jsx index 3c2dc7649..e0f1f03bc 100644 --- a/src/course-outline/CourseOutline.test.jsx +++ b/src/course-outline/CourseOutline.test.jsx @@ -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('', () => { beforeEach(async () => { initializeMockApp({ authenticatedUser: { - userId: 3, + userId, username: 'abc123', administrator: true, roles: [], @@ -102,6 +107,14 @@ describe('', () => { 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('', () => { }); }); + it('should render permissionDenied if incorrect permissions', async () => { + const { getByTestId } = render(); + 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(); diff --git a/src/grading-settings/GradingSettings.jsx b/src/grading-settings/GradingSettings.jsx index 827486e58..6bcf6bca3 100644 --- a/src/grading-settings/GradingSettings.jsx +++ b/src/grading-settings/GradingSettings.jsx @@ -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 ( + + ); + } + 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} /> {courseSettingsData.creditEligibilityEnabled && courseSettingsData.isCreditCourse && ( @@ -170,6 +188,7 @@ const GradingSettings = ({ intl, courseId }) => { minimumGradeCredit={minimumGradeCredit} setGradingData={setGradingData} setShowSuccessAlert={setShowSuccessAlert} + viewOnly={viewOnly} /> )} @@ -183,6 +202,7 @@ const GradingSettings = ({ intl, courseId }) => { gracePeriod={gracePeriod} setGradingData={setGradingData} setShowSuccessAlert={setShowSuccessAlert} + viewOnly={viewOnly} />
@@ -201,11 +221,13 @@ const GradingSettings = ({ intl, courseId }) => { setGradingData={setGradingData} courseAssignmentLists={courseAssignmentLists} setShowSuccessAlert={setShowSuccessAlert} + viewOnly={viewOnly} /> diff --git a/src/grading-settings/GradingSettings.test.jsx b/src/grading-settings/GradingSettings.test.jsx index 955b33d01..28f368104 100644 --- a/src/grading-settings/GradingSettings.test.jsx +++ b/src/grading-settings/GradingSettings.test.jsx @@ -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 = () => ( @@ -28,7 +34,7 @@ describe('', () => { beforeEach(() => { initializeMockApp({ authenticatedUser: { - userId: 3, + userId, username: 'abc123', administrator: true, roles: [], @@ -40,6 +46,25 @@ describe('', () => { 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('', () => { }); }); + it('should render without errors if access controlled by permissions', async () => { + const { getByText, getAllByText, getAllByTestId } = render(); + 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(); + 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(); await waitFor(() => { @@ -76,6 +133,7 @@ describe('', () => { expect(segmentInput).toHaveValue('a'); }); }); + it('should save segment input changes and display saving message', async () => { const { getByText, getAllByTestId } = render(); await waitFor(() => { @@ -88,4 +146,23 @@ describe('', () => { expect(getByText(messages.buttonSavingText.defaultMessage)).toBeInTheDocument(); }); }); + + it('should show disabled button to update segments', async () => { + const { getAllByTestId } = render(); + 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); + }); + }); }); diff --git a/src/grading-settings/assignment-section/assignments/AssignmentItem.jsx b/src/grading-settings/assignment-section/assignments/AssignmentItem.jsx index 6c355c28a..e67df21ce 100644 --- a/src/grading-settings/assignment-section/assignments/AssignmentItem.jsx +++ b/src/grading-settings/assignment-section/assignments/AssignmentItem.jsx @@ -20,6 +20,7 @@ const AssignmentItem = ({ secondErrorMsg, gradeField, trailingElement, + viewOnly, }) => (
  • {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; diff --git a/src/grading-settings/assignment-section/assignments/AssignmentTypeName.jsx b/src/grading-settings/assignment-section/assignments/AssignmentTypeName.jsx index 918160b46..a5c8b8b8d 100644 --- a/src/grading-settings/assignment-section/assignments/AssignmentTypeName.jsx +++ b/src/grading-settings/assignment-section/assignments/AssignmentTypeName.jsx @@ -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} /> {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); diff --git a/src/grading-settings/assignment-section/index.jsx b/src/grading-settings/assignment-section/index.jsx index 8c329e70f..2e5450823 100644 --- a/src/grading-settings/assignment-section/index.jsx +++ b/src/grading-settings/assignment-section/index.jsx @@ -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} /> handleAssignmentChange(e, gradeField.id)} + viewOnly={viewOnly} /> handleAssignmentChange(e, gradeField.id)} errorEffort={errorList[`${weight}-${gradeField.id}`]} trailingElement="%" + viewOnly={viewOnly} /> handleAssignmentChange(e, gradeField.id)} errorEffort={errorList[`${minCount}-${gradeField.id}`]} + viewOnly={viewOnly} /> {showDefinedCaseAlert && ( @@ -185,6 +191,7 @@ const AssignmentSection = ({ variant="outline-primary" size="sm" onClick={() => handleRemoveAssignment(gradeField.id)} + disabled={viewOnly} > {intl.formatMessage(messages.assignmentDeleteButton)} @@ -210,6 +217,7 @@ AssignmentSection.propTypes = { graders: PropTypes.arrayOf( PropTypes.shape(defaultAssignmentsPropTypes), ), + viewOnly: PropTypes.bool.isRequired, }; export default injectIntl(AssignmentSection); diff --git a/src/grading-settings/credit-section/CreditSection.test.jsx b/src/grading-settings/credit-section/CreditSection.test.jsx index 548e5a6ff..37d66275e 100644 --- a/src/grading-settings/credit-section/CreditSection.test.jsx +++ b/src/grading-settings/credit-section/CreditSection.test.jsx @@ -18,6 +18,7 @@ const RootWrapper = (props = {}) => ( minimumGradeCredit={0.1} setGradingData={jest.fn()} setShowSuccessAlert={jest.fn()} + viewOnly={false} {...props} /> @@ -38,6 +39,14 @@ describe('', () => { 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(); + await waitFor(() => { + const inputElement = getByTestId('minimum-grade-credit-input'); + expect(inputElement.disabled).toBe(true); }); }); }); diff --git a/src/grading-settings/credit-section/index.jsx b/src/grading-settings/credit-section/index.jsx index 03b60ac4f..49d912696 100644 --- a/src/grading-settings/credit-section/index.jsx +++ b/src/grading-settings/credit-section/index.jsx @@ -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} /> {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); diff --git a/src/grading-settings/deadline-section/DeadlineSection.test.jsx b/src/grading-settings/deadline-section/DeadlineSection.test.jsx index bdacb4a6c..89519a359 100644 --- a/src/grading-settings/deadline-section/DeadlineSection.test.jsx +++ b/src/grading-settings/deadline-section/DeadlineSection.test.jsx @@ -22,6 +22,7 @@ const RootWrapper = (props = {}) => ( setShowSavePrompt={jest.fn()} setGradingData={jest.fn()} setShowSuccessAlert={jest.fn()} + viewOnly={false} {...props} /> @@ -46,6 +47,7 @@ describe('', () => { 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('', () => { 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(); + await waitFor(() => { + const inputElement = getByTestId('deadline-period-input'); + expect(inputElement.disabled).toEqual(true); + }); + }); }); diff --git a/src/grading-settings/deadline-section/index.jsx b/src/grading-settings/deadline-section/index.jsx index 398f1f06e..676671bfa 100644 --- a/src/grading-settings/deadline-section/index.jsx +++ b/src/grading-settings/deadline-section/index.jsx @@ -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} /> {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); diff --git a/src/grading-settings/grading-scale/GradingScale.jsx b/src/grading-settings/grading-scale/GradingScale.jsx index 605536ac2..4b3bc3b48 100644 --- a/src/grading-settings/grading-scale/GradingScale.jsx +++ b/src/grading-settings/grading-scale/GradingScale.jsx @@ -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 = ({ = 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); diff --git a/src/grading-settings/grading-scale/GradingScale.test.jsx b/src/grading-settings/grading-scale/GradingScale.test.jsx index f311c1757..98340ad7d 100644 --- a/src/grading-settings/grading-scale/GradingScale.test.jsx +++ b/src/grading-settings/grading-scale/GradingScale.test.jsx @@ -16,7 +16,7 @@ const sortedGrades = [ { current: 20, previous: 0 }, ]; -const RootWrapper = () => ( +const RootWrapper = (viewOnly = { viewOnly: false }) => ( ( setGradingData={jest.fn()} setOverrideInternetConnectionAlert={jest.fn()} setEligibleGrade={jest.fn()} + {...viewOnly} /> ); @@ -73,6 +74,26 @@ describe('', () => { }); }); + it('should not disable new grading segment button when viewOnly=false', async () => { + const { getAllByTestId } = render(); + 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(); + 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(); await waitFor(() => { diff --git a/src/header/Header.jsx b/src/header/Header.jsx index f3f832722..43bd3afb1 100644 --- a/src/header/Header.jsx +++ b/src/header/Header.jsx @@ -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, }), }, ); diff --git a/src/header/utils.js b/src/header/utils.js index 26c3929b0..4100aed67 100644 --- a/src/header/utils.js +++ b/src/header/utils.js @@ -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}`, diff --git a/src/header/utils.test.js b/src/header/utils.test.js index bad310ce5..227db9ad5 100644 --- a/src/header/utils.test.js +++ b/src/header/utils.test.js @@ -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', () => {