From c754a5e519e42d4416e4fc719ecb3a00e96dca6a Mon Sep 17 00:00:00 2001
From: hilary sinkoff <10408711+hsinkoff@users.noreply.github.com>
Date: Thu, 15 Feb 2024 14:52:18 -0600
Subject: [PATCH] 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
---
src/course-outline/CourseOutline.jsx | 15 ++++
src/course-outline/CourseOutline.test.jsx | 22 +++++-
src/grading-settings/GradingSettings.jsx | 22 ++++++
src/grading-settings/GradingSettings.test.jsx | 79 ++++++++++++++++++-
.../assignments/AssignmentItem.jsx | 3 +
.../assignments/AssignmentTypeName.jsx | 4 +-
.../assignment-section/index.jsx | 8 ++
.../credit-section/CreditSection.test.jsx | 9 +++
src/grading-settings/credit-section/index.jsx | 4 +-
.../deadline-section/DeadlineSection.test.jsx | 9 +++
.../deadline-section/index.jsx | 4 +-
.../grading-scale/GradingScale.jsx | 4 +-
.../grading-scale/GradingScale.test.jsx | 23 +++++-
src/header/Header.jsx | 7 +-
src/header/utils.js | 45 +++++++----
src/header/utils.test.js | 58 ++++++++++++--
16 files changed, 289 insertions(+), 27 deletions(-)
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', () => {