diff --git a/src/CourseAuthoringPage.jsx b/src/CourseAuthoringPage.jsx
index c442f9406..c6feb9ebb 100644
--- a/src/CourseAuthoringPage.jsx
+++ b/src/CourseAuthoringPage.jsx
@@ -14,6 +14,8 @@ import PermissionDeniedAlert from './generic/PermissionDeniedAlert';
import { getCourseAppsApiStatus } from './pages-and-resources/data/selectors';
import { RequestStatus } from './data/constants';
import Loading from './generic/Loading';
+import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from './generic/data/thunks';
+import { getUserPermissions } from './generic/data/selectors';
const AppHeader = ({
courseNumber, courseOrg, courseTitle, courseId,
@@ -40,9 +42,14 @@ AppHeader.defaultProps = {
const CourseAuthoringPage = ({ courseId, children }) => {
const dispatch = useDispatch();
+ const userPermissions = useSelector(getUserPermissions);
useEffect(() => {
dispatch(fetchCourseDetail(courseId));
+ dispatch(fetchUserPermissionsEnabledFlag());
+ if (!userPermissions) {
+ dispatch(fetchUserPermissionsQuery(courseId));
+ }
}, [courseId]);
const courseDetail = useModel('courseDetails', courseId);
diff --git a/src/advanced-settings/AdvancedSettings.jsx b/src/advanced-settings/AdvancedSettings.jsx
index 7611593a9..19452d2b3 100644
--- a/src/advanced-settings/AdvancedSettings.jsx
+++ b/src/advanced-settings/AdvancedSettings.jsx
@@ -25,6 +25,9 @@ import validateAdvancedSettingsData from './utils';
import messages from './messages';
import ModalError from './modal-error/ModalError';
import getPageHeadTitle from '../generic/utils';
+import { useUserPermissions } from '../generic/hooks';
+import { getUserPermissionsEnabled } from '../generic/data/selectors';
+import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
const AdvancedSettings = ({ intl, courseId }) => {
const dispatch = useDispatch();
@@ -41,6 +44,13 @@ const AdvancedSettings = ({ intl, courseId }) => {
const courseDetails = useModel('courseDetails', courseId);
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
+ const { checkPermission } = useUserPermissions();
+ const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
+ const viewOnly = checkPermission('view_course_settings');
+ const showPermissionDeniedAlert = userPermissionsEnabled && (
+ !checkPermission('manage_advanced_settings') && !checkPermission('view_course_settings')
+ );
+
useEffect(() => {
dispatch(fetchCourseAppSettings(courseId));
dispatch(fetchProctoringExamErrors(courseId));
@@ -83,6 +93,11 @@ const AdvancedSettings = ({ intl, courseId }) => {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>>;
}
+ if (showPermissionDeniedAlert) {
+ return (
+
+ );
+ }
if (loadingSettingsStatus === RequestStatus.DENIED) {
return (
@@ -215,6 +230,7 @@ const AdvancedSettings = ({ intl, courseId }) => {
handleBlur={handleSettingBlur}
isEditableState={isEditableState}
setIsEditableState={setIsEditableState}
+ disableForm={viewOnly}
/>
);
})}
diff --git a/src/advanced-settings/AdvancedSettings.test.jsx b/src/advanced-settings/AdvancedSettings.test.jsx
index cc5144c64..3f4f5fd2a 100644
--- a/src/advanced-settings/AdvancedSettings.test.jsx
+++ b/src/advanced-settings/AdvancedSettings.test.jsx
@@ -3,7 +3,11 @@ import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
-import { render, fireEvent, waitFor } from '@testing-library/react';
+import {
+ render,
+ fireEvent,
+ waitFor,
+} from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import initializeStore from '../store';
@@ -13,11 +17,15 @@ import { getCourseAdvancedSettingsApiUrl } from './data/api';
import { updateCourseAppSetting } from './data/thunks';
import AdvancedSettings from './AdvancedSettings';
import messages from './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: ['view_course_settings', 'manage_advanced_settings'] };
// Mock the TextareaAutosize component
jest.mock('react-textarea-autosize', () => jest.fn((props) => (
@@ -43,11 +51,23 @@ const RootWrapper = () => (
);
+const permissionsMockStore = async (permissions) => {
+ axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, permissions);
+ axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
+ await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
+ await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
+};
+
+const permissionDisabledMockStore = async () => {
+ axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: false });
+ await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
+};
+
describe('
', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
- userId: 3,
+ userId,
username: 'abc123',
administrator: true,
roles: [],
@@ -58,7 +78,9 @@ describe('
', () => {
axiosMock
.onGet(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`)
.reply(200, advancedSettingsMock);
+ permissionsMockStore(userPermissionsData);
});
+
it('should render without errors', async () => {
const { getByText } = render(
);
await waitFor(() => {
@@ -161,4 +183,29 @@ describe('
', () => {
await executeThunk(updateCourseAppSetting(courseId, [3, 2, 1]), store.dispatch);
expect(getByText('Your policy changes have been saved.')).toBeInTheDocument();
});
+ it('should shows the PermissionDeniedAlert when there are not the right user permissions', async () => {
+ const permissionsData = { permissions: ['view'] };
+ await permissionsMockStore(permissionsData);
+
+ const { queryByText } = render(
);
+ await waitFor(() => {
+ const permissionDeniedAlert = queryByText('You are not authorized to view this page. If you feel you should have access, please reach out to your course team admin to be given access.');
+ expect(permissionDeniedAlert).toBeInTheDocument();
+ });
+ });
+ it('should not show the PermissionDeniedAlert when the User Permissions Flag is not enabled', async () => {
+ await permissionDisabledMockStore();
+
+ const { queryByText } = render(
);
+ const permissionDeniedAlert = queryByText('You are not authorized to view this page. If you feel you should have access, please reach out to your course team admin to be given access.');
+ expect(permissionDeniedAlert).not.toBeInTheDocument();
+ });
+ it('should be view only if the permission is set for viewOnly', async () => {
+ const permissions = { permissions: ['view_course_settings'] };
+ await permissionsMockStore(permissions);
+ const { getByLabelText } = render(
);
+ await waitFor(() => {
+ expect(getByLabelText('Advanced Module List')).toBeDisabled();
+ });
+ });
});
diff --git a/src/advanced-settings/setting-card/SettingCard.jsx b/src/advanced-settings/setting-card/SettingCard.jsx
index e522963f8..bc1cf89dd 100644
--- a/src/advanced-settings/setting-card/SettingCard.jsx
+++ b/src/advanced-settings/setting-card/SettingCard.jsx
@@ -27,6 +27,7 @@ const SettingCard = ({
setIsEditableState,
// injected
intl,
+ disableForm,
}) => {
const { deprecated, help, displayName } = settingData;
const initialValue = JSON.stringify(settingData.value, null, 4);
@@ -100,6 +101,7 @@ const SettingCard = ({
onChange={handleSettingChange}
aria-label={displayName}
onBlur={handleCardBlur}
+ disabled={disableForm}
/>
@@ -135,6 +137,7 @@ SettingCard.propTypes = {
saveSettingsPrompt: PropTypes.bool.isRequired,
isEditableState: PropTypes.bool.isRequired,
setIsEditableState: PropTypes.func.isRequired,
+ disableForm: PropTypes.bool.isRequired,
};
export default injectIntl(SettingCard);
diff --git a/src/course-team/CourseTeam.jsx b/src/course-team/CourseTeam.jsx
index 937a76620..48282fdd0 100644
--- a/src/course-team/CourseTeam.jsx
+++ b/src/course-team/CourseTeam.jsx
@@ -18,12 +18,14 @@ import AddUserForm from './add-user-form/AddUserForm';
import AddTeamMember from './add-team-member/AddTeamMember';
import CourseTeamMember from './course-team-member/CourseTeamMember';
import InfoModal from './info-modal/InfoModal';
-import { useCourseTeam, useUserPermissions } from './hooks';
+import { useCourseTeam } from './hooks';
+import { useUserPermissions } from '../generic/hooks';
import getPageHeadTitle from '../generic/utils';
const CourseTeam = ({ courseId }) => {
const intl = useIntl();
const courseDetails = useModel('courseDetails', courseId);
+
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
const {
@@ -55,10 +57,10 @@ const CourseTeam = ({ courseId }) => {
} = useCourseTeam({ intl, courseId });
const {
- hasPermissions,
+ checkPermission,
} = useUserPermissions();
- const hasManageAllUsersPerm = hasPermissions('manage_all_users');
+ const hasManageAllUsersPerm = checkPermission('manage_all_users');
if (isLoading) {
// eslint-disable-next-line react/jsx-no-useless-fragment
diff --git a/src/course-team/CourseTeam.test.jsx b/src/course-team/CourseTeam.test.jsx
index 7bff97e08..c770808da 100644
--- a/src/course-team/CourseTeam.test.jsx
+++ b/src/course-team/CourseTeam.test.jsx
@@ -15,6 +15,7 @@ import initializeStore from '../store';
import { courseTeamMock, courseTeamWithOneUser, courseTeamWithoutUsers } from './__mocks__';
import { getCourseTeamApiUrl, updateCourseTeamUserApiUrl } from './data/api';
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api';
+import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
import CourseTeam from './CourseTeam';
import messages from './messages';
import { USER_ROLES } from '../constants';
@@ -180,7 +181,7 @@ describe('
', () => {
});
});
- it('not displays "Add New Member" and AddTeamMember component when isAllowActions or hasManageAllUsersPerm is false', async () => {
+ it('not displays "Add New Member" and AddTeamMember component when isAllowActions is false and hasManageAllUsersPerm is false', async () => {
cleanup();
axiosMock
.onGet(getCourseTeamApiUrl(courseId))
@@ -190,13 +191,37 @@ describe('
', () => {
});
axiosMock
.onGet(getUserPermissionsEnabledFlagUrl)
- .reply(200, { enabled: false });
+ .reply(200, { enabled: true });
- const { queryByRole, queryByTestId } = render(
);
+ const { queryByRole, queryByText } = render(
);
await waitFor(() => {
expect(queryByRole('button', { name: messages.addNewMemberButton.defaultMessage })).not.toBeInTheDocument();
- expect(queryByTestId('add-team-member')).not.toBeInTheDocument();
+ expect(queryByText('add-team-member')).not.toBeInTheDocument();
+ });
+ });
+
+ it('displays "Add New Member" and AddTeamMember component when hasManageAllUsersPerm is true', async () => {
+ cleanup();
+ axiosMock
+ .onGet(getCourseTeamApiUrl(courseId))
+ .reply(200, {
+ ...courseTeamWithOneUser,
+ allowActions: false,
+ });
+ axiosMock
+ .onGet(getUserPermissionsEnabledFlagUrl)
+ .reply(200, { enabled: true });
+ axiosMock
+ .onGet(getUserPermissionsUrl(courseId, userId))
+ .reply(200, userPermissionsData);
+ await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
+ await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
+
+ const { getByRole } = render(
);
+
+ await waitFor(() => {
+ expect(getByRole('button', { name: messages.addNewMemberButton.defaultMessage })).toBeInTheDocument();
});
});
diff --git a/src/course-team/hooks.jsx b/src/course-team/hooks.jsx
index 6ae70d452..6fceb5aac 100644
--- a/src/course-team/hooks.jsx
+++ b/src/course-team/hooks.jsx
@@ -6,8 +6,6 @@ import { useToggle } from '@edx/paragon';
import { USER_ROLES } from '../constants';
import { RequestStatus } from '../data/constants';
import { useModel } from '../generic/model-store';
-import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
-import { getUserPermissions, getUserPermissionsEnabled } from '../generic/data/selectors';
import {
changeRoleTeamUserQuery,
createCourseTeamQuery,
@@ -98,8 +96,6 @@ const useCourseTeam = ({ courseId }) => {
useEffect(() => {
dispatch(fetchCourseTeamQuery(courseId));
- dispatch(fetchUserPermissionsEnabledFlag());
- dispatch(fetchUserPermissionsQuery(courseId));
}, [courseId]);
useEffect(() => {
@@ -139,20 +135,5 @@ const useCourseTeam = ({ courseId }) => {
};
};
-const useUserPermissions = () => {
- const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
- const userPermissions = useSelector(getUserPermissions);
- const hasPermissions = (checkPermissions) => {
- if (userPermissionsEnabled) {
- return userPermissions?.includes(checkPermissions);
- }
- return false;
- };
-
- return {
- hasPermissions,
- };
-};
-
// eslint-disable-next-line import/prefer-default-export
-export { useCourseTeam, useUserPermissions };
+export { useCourseTeam };
diff --git a/src/course-updates/CourseUpdates.jsx b/src/course-updates/CourseUpdates.jsx
index ce63ade49..85b6b7db9 100644
--- a/src/course-updates/CourseUpdates.jsx
+++ b/src/course-updates/CourseUpdates.jsx
@@ -8,12 +8,12 @@ import {
} from '@edx/paragon';
import { Add as AddIcon } from '@edx/paragon/icons';
import { useSelector } from 'react-redux';
-
import { useModel } from '../generic/model-store';
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
import ProcessingNotification from '../generic/processing-notification';
import SubHeader from '../generic/sub-header/SubHeader';
import InternetConnectionAlert from '../generic/internet-connection-alert';
+import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
import { RequestStatus } from '../data/constants';
import CourseHandouts from './course-handouts/CourseHandouts';
import CourseUpdate from './course-update/CourseUpdate';
@@ -25,6 +25,8 @@ import { useCourseUpdates } from './hooks';
import { getLoadingStatuses, getSavingStatuses } from './data/selectors';
import { matchesAnyStatus } from './utils';
import getPageHeadTitle from '../generic/utils';
+import { getUserPermissionsEnabled } from '../generic/data/selectors';
+import { useUserPermissions } from '../generic/hooks';
const CourseUpdates = ({ courseId }) => {
const intl = useIntl();
@@ -60,7 +62,15 @@ const CourseUpdates = ({ courseId }) => {
const anyStatusFailed = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.FAILED);
const anyStatusInProgress = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.IN_PROGRESS);
const anyStatusPending = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.PENDING);
+ const { checkPermission } = useUserPermissions();
+ const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
+ const showPermissionDeniedAlert = userPermissionsEnabled && !checkPermission('manage_content');
+ if (showPermissionDeniedAlert) {
+ return (
+
+ );
+ }
return (
<>
diff --git a/src/course-updates/CourseUpdates.test.jsx b/src/course-updates/CourseUpdates.test.jsx
index d136665e8..9be8cc4fc 100644
--- a/src/course-updates/CourseUpdates.test.jsx
+++ b/src/course-updates/CourseUpdates.test.jsx
@@ -22,11 +22,16 @@ import { executeThunk } from '../utils';
import { courseUpdatesMock, courseHandoutsMock } from './__mocks__';
import CourseUpdates from './CourseUpdates';
import messages from './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: ['manage_content'] };
+const wrongUserPermissionsData = { permissions: ['wrong_permission'] };
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
@@ -61,7 +66,7 @@ const RootWrapper = () => (
);
describe('', () => {
- beforeEach(() => {
+ beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
@@ -79,6 +84,7 @@ describe('', () => {
axiosMock
.onGet(getCourseHandoutApiUrl(courseId))
.reply(200, courseHandoutsMock);
+ axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: false });
});
it('render CourseUpdates component correctly', async () => {
@@ -162,6 +168,26 @@ describe('', () => {
expect(queryByText(courseHandoutsMock.data)).not.toBeInTheDocument();
});
+ it('should shows PermissionDeniedAlert if there are no right User Permissions', async () => {
+ const { getByTestId } = render();
+
+ axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, wrongUserPermissionsData);
+ axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
+ await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
+ await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
+ expect(getByTestId('permissionDeniedAlert')).toBeVisible();
+ });
+
+ it('should not show PermissionDeniedAlert if User Permissions are the correct ones', async () => {
+ const { queryByTestId } = render();
+
+ axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, userPermissionsData);
+ axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
+ await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
+ await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
+ expect(queryByTestId('permissionDeniedAlert')).not.toBeInTheDocument();
+ });
+
it('Add new update form is visible after clicking "New update" button', async () => {
const { getByText, getByRole, getAllByTestId } = render();
diff --git a/src/files-and-videos/files-page/FilesPage.jsx b/src/files-and-videos/files-page/FilesPage.jsx
index 1d9c7872b..82f64c040 100644
--- a/src/files-and-videos/files-page/FilesPage.jsx
+++ b/src/files-and-videos/files-page/FilesPage.jsx
@@ -30,6 +30,9 @@ import {
import { getFileSizeToClosestByte } from '../../utils';
import FileThumbnail from './FileThumbnail';
import FileInfoModalSidebar from './FileInfoModalSidebar';
+import { useUserPermissions } from '../../generic/hooks';
+import { getUserPermissionsEnabled } from '../../generic/data/selectors';
+import PermissionDeniedAlert from '../../generic/PermissionDeniedAlert';
const FilesPage = ({
courseId,
@@ -39,6 +42,9 @@ const FilesPage = ({
const dispatch = useDispatch();
const courseDetails = useModel('courseDetails', courseId);
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading));
+ const { checkPermission } = useUserPermissions();
+ const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
+ const showPermissionDeniedAlert = userPermissionsEnabled && !checkPermission('manage_content');
useEffect(() => {
dispatch(fetchAssets(courseId));
@@ -160,6 +166,11 @@ const FilesPage = ({
{ ...accessColumn },
];
+ if (showPermissionDeniedAlert) {
+ return (
+
+ );
+ }
if (loadingStatus === RequestStatus.DENIED) {
return (
diff --git a/src/files-and-videos/files-page/FilesPage.test.jsx b/src/files-and-videos/files-page/FilesPage.test.jsx
index ec15f993f..a4dba005d 100644
--- a/src/files-and-videos/files-page/FilesPage.test.jsx
+++ b/src/files-and-videos/files-page/FilesPage.test.jsx
@@ -39,10 +39,16 @@ import {
} from './data/thunks';
import { getAssetsUrl } from './data/api';
import messages from '../generic/messages';
+import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../../generic/data/api';
+import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../../generic/data/thunks';
let axiosMock;
let store;
let file;
+const userId = 3;
+const wrongUserPermissionsData = { permissions: ['wrong_permission'] };
+const userPermissionsData = { permissions: ['manage_content'] };
+
ReactDOM.createPortal = jest.fn(node => node);
jest.mock('file-saver');
@@ -68,6 +74,8 @@ const mockStore = async (
}
renderComponent();
await executeThunk(fetchAssets(courseId), store.dispatch);
+ await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
+ await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
};
const emptyMockStore = async (status) => {
@@ -75,6 +83,27 @@ const emptyMockStore = async (status) => {
axiosMock.onGet(fetchAssetsUrl).reply(getStatusValue(status), generateEmptyApiResponse());
renderComponent();
await executeThunk(fetchAssets(courseId), store.dispatch);
+ await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
+ await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
+};
+
+const wrongUserPermissionsMockStore = async () => {
+ axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, wrongUserPermissionsData);
+ axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
+ await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
+ await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
+};
+
+const disabledUserPermissionsFlagMockStore = async () => {
+ axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: false });
+ await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
+};
+
+const userPermissionsMockStore = async () => {
+ axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, userPermissionsData);
+ axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
+ await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
+ await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
};
describe('FilesAndUploads', () => {
@@ -82,7 +111,7 @@ describe('FilesAndUploads', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
- userId: 3,
+ userId,
username: 'abc123',
administrator: false,
roles: [],
@@ -100,6 +129,21 @@ describe('FilesAndUploads', () => {
file = new File(['(⌐□_□)'], 'download.png', { type: 'image/png' });
});
+ it('should shows PermissionDeniedAlert if there are no right User Permissions', async () => {
+ renderComponent();
+ await wrongUserPermissionsMockStore();
+ expect(screen.getByTestId('permissionDeniedAlert')).toBeVisible();
+ });
+ it('should not show PermissionDeniedAlert if User Permissions Flag is not enabled', async () => {
+ renderComponent();
+ await disabledUserPermissionsFlagMockStore();
+ expect(screen.queryByText('permissionDeniedAlert')).not.toBeInTheDocument();
+ });
+ it('should not show PermissionDeniedAlert if User Permissions Flag is enabled and permissions are correct', async () => {
+ renderComponent();
+ await userPermissionsMockStore();
+ expect(screen.queryByText('permissionDeniedAlert')).not.toBeInTheDocument();
+ });
it('should return placeholder component', async () => {
await mockStore(RequestStatus.DENIED);
diff --git a/src/files-and-videos/videos-page/VideosPage.jsx b/src/files-and-videos/videos-page/VideosPage.jsx
index ff7468999..48e123f3e 100644
--- a/src/files-and-videos/videos-page/VideosPage.jsx
+++ b/src/files-and-videos/videos-page/VideosPage.jsx
@@ -43,6 +43,9 @@ import VideoThumbnail from './VideoThumbnail';
import { getFormattedDuration, resampleFile } from './data/utils';
import FILES_AND_UPLOAD_TYPE_FILTERS from '../generic/constants';
import VideoInfoModalSidebar from './info-sidebar';
+import { useUserPermissions } from '../../generic/hooks';
+import { getUserPermissionsEnabled } from '../../generic/data/selectors';
+import PermissionDeniedAlert from '../../generic/PermissionDeniedAlert';
const VideosPage = ({
courseId,
@@ -53,6 +56,9 @@ const VideosPage = ({
const [isTranscriptSettingsOpen, openTranscriptSettings, closeTranscriptSettings] = useToggle(false);
const courseDetails = useModel('courseDetails', courseId);
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading));
+ const { checkPermission } = useUserPermissions();
+ const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
+ const showPermissionDeniedAlert = userPermissionsEnabled && !checkPermission('manage_content');
useEffect(() => {
dispatch(fetchVideos(courseId));
@@ -179,6 +185,11 @@ const VideosPage = ({
{ ...processingStatusColumn },
];
+ if (showPermissionDeniedAlert) {
+ return (
+
+ );
+ }
if (loadingStatus === RequestStatus.DENIED) {
return (
diff --git a/src/files-and-videos/videos-page/VideosPage.test.jsx b/src/files-and-videos/videos-page/VideosPage.test.jsx
index b4ce8ca07..38a3bceae 100644
--- a/src/files-and-videos/videos-page/VideosPage.test.jsx
+++ b/src/files-and-videos/videos-page/VideosPage.test.jsx
@@ -39,10 +39,15 @@ import {
import { getVideosUrl, getCourseVideosApiUrl, getApiBaseUrl } from './data/api';
import videoMessages from './messages';
import messages from '../generic/messages';
+import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../../generic/data/api';
+import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../../generic/data/thunks';
let axiosMock;
let store;
let file;
+const userId = 3;
+const wrongUserPermissionsData = { permissions: ['wrong_permission'] };
+const userPermissionsData = { permissions: ['manage_content'] };
jest.mock('file-saver');
const renderComponent = () => {
@@ -55,9 +60,7 @@ const renderComponent = () => {
);
};
-const mockStore = async (
- status,
-) => {
+const mockStore = async (status) => {
const fetchVideosUrl = getVideosUrl(courseId);
const videosData = generateFetchVideosApiResponse();
axiosMock.onGet(fetchVideosUrl).reply(getStatusValue(status), videosData);
@@ -68,6 +71,8 @@ const mockStore = async (
renderComponent();
await executeThunk(fetchVideos(courseId), store.dispatch);
+ await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
+ await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
};
const emptyMockStore = async (status) => {
@@ -75,6 +80,27 @@ const emptyMockStore = async (status) => {
axiosMock.onGet(fetchVideosUrl).reply(getStatusValue(status), generateEmptyApiResponse());
renderComponent();
await executeThunk(fetchVideos(courseId), store.dispatch);
+ await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
+ await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
+};
+
+const wrongUserPermissionsMockStore = async () => {
+ axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, wrongUserPermissionsData);
+ axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
+ await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
+ await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
+};
+
+const disabledUserPermissionsFlagMockStore = async () => {
+ axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: false });
+ await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
+};
+
+const userPermissionsMockStore = async () => {
+ axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, userPermissionsData);
+ axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
+ await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
+ await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
};
describe('Videos page', () => {
@@ -82,7 +108,7 @@ describe('Videos page', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
- userId: 3,
+ userId,
username: 'abc123',
administrator: false,
roles: [],
@@ -146,13 +172,31 @@ describe('Videos page', () => {
expect(screen.getByTestId('files-data-table')).toBeVisible();
});
+
+ it('should shows PermissionDeniedAlert if there are no right User Permissions', async () => {
+ renderComponent();
+ await wrongUserPermissionsMockStore();
+ expect(screen.getByTestId('permissionDeniedAlert')).toBeVisible();
+ });
+
+ it('should not show PermissionDeniedAlert if User Permissions Flag is not enabled', async () => {
+ renderComponent();
+ await disabledUserPermissionsFlagMockStore();
+ expect(screen.queryByText('permissionDeniedAlert')).not.toBeInTheDocument();
+ });
+
+ it('should not show PermissionDeniedAlert if User Permissions Flag is enabled and permission is correct', async () => {
+ renderComponent();
+ await userPermissionsMockStore();
+ expect(screen.queryByText('permissionDeniedAlert')).not.toBeInTheDocument();
+ });
});
describe('valid videos', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
- userId: 3,
+ userId,
username: 'abc123',
administrator: false,
roles: [],
diff --git a/src/generic/create-or-rerun-course/factories/mockApiResponses.jsx b/src/generic/create-or-rerun-course/factories/mockApiResponses.jsx
index ca3e0b347..c559779b9 100644
--- a/src/generic/create-or-rerun-course/factories/mockApiResponses.jsx
+++ b/src/generic/create-or-rerun-course/factories/mockApiResponses.jsx
@@ -16,6 +16,8 @@ export const initialState = {
},
organizations: ['krisEdx', 'krisEd', 'DeveloperInc', 'importMit', 'testX', 'edX', 'developerInb'],
savingStatus: '',
+ userPermissions: [],
+ userPermissionsEnabled: false,
},
studioHome: {
loadingStatuses: {
diff --git a/src/generic/data/api.js b/src/generic/data/api.js
index 71a10e34e..47d9dca4b 100644
--- a/src/generic/data/api.js
+++ b/src/generic/data/api.js
@@ -48,7 +48,6 @@ export async function createOrRerunCourse(courseData) {
/**
* Get user course roles permissions.
* @param {string} courseId
- * @param {string} userId
* @returns {Promise