From a518fada29bc7317677463244a5995a5b59688d9 Mon Sep 17 00:00:00 2001 From: hilary sinkoff <10408711+hsinkoff@users.noreply.github.com> Date: Tue, 6 Feb 2024 15:02:19 -0600 Subject: [PATCH] feat: Access for Import/Export Pages Based on Permissions (#804) * feat: import/export page access based on permissions --- src/export-page/CourseExportPage.jsx | 20 +++++- src/export-page/CourseExportPage.test.jsx | 70 ++++++++++++++++++- .../export-stepper/ExportStepper.jsx | 5 +- .../export-stepper/ExportStepper.test.jsx | 31 +++++++- .../__mocks__/exportStepperPage.js | 4 ++ .../export-stepper/__mocks__/index.js | 2 + src/header/Header.jsx | 27 ++++--- src/header/utils.js | 18 +++-- src/header/utils.test.js | 4 +- src/import-page/CourseImportPage.jsx | 19 ++++- src/import-page/CourseImportPage.test.jsx | 37 +++++++++- src/import-page/file-section/FileSection.jsx | 41 ++++++----- .../file-section/FileSection.test.jsx | 18 ++++- src/import-page/file-section/messages.js | 4 ++ .../PagesAndResources.test.jsx | 6 ++ 15 files changed, 257 insertions(+), 49 deletions(-) create mode 100644 src/export-page/export-stepper/__mocks__/exportStepperPage.js create mode 100644 src/export-page/export-stepper/__mocks__/index.js diff --git a/src/export-page/CourseExportPage.jsx b/src/export-page/CourseExportPage.jsx index 55ae67064..bbaed4bab 100644 --- a/src/export-page/CourseExportPage.jsx +++ b/src/export-page/CourseExportPage.jsx @@ -25,6 +25,9 @@ import { updateExportTriggered, updateSavingStatus, updateSuccessDate } from './ import ExportModalError from './export-modal-error/ExportModalError'; import ExportFooter from './export-footer/ExportFooter'; import ExportStepper from './export-stepper/ExportStepper'; +import { useUserPermissions } from '../generic/hooks'; +import { getUserPermissionsEnabled } from '../generic/data/selectors'; +import PermissionDeniedAlert from '../generic/PermissionDeniedAlert'; const CourseExportPage = ({ intl, courseId }) => { const dispatch = useDispatch(); @@ -38,6 +41,14 @@ const CourseExportPage = ({ intl, courseId }) => { const isShowExportButton = !exportTriggered || errorMessage || currentStage === EXPORT_STAGES.SUCCESS; const anyRequestFailed = savingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.FAILED; const anyRequestInProgress = savingStatus === RequestStatus.PENDING || loadingStatus === RequestStatus.IN_PROGRESS; + const { checkPermission } = useUserPermissions(); + const userPermissionsEnabled = useSelector(getUserPermissionsEnabled); + const hasExportPermissions = !userPermissionsEnabled || ( + userPermissionsEnabled && (checkPermission('manage_course_settings') || checkPermission('view_course_settings')) + ); + const viewOnly = !userPermissionsEnabled || ( + userPermissionsEnabled && checkPermission('view_course_settings') && !checkPermission('manage_course_settings') + ); useEffect(() => { const cookieData = cookies.get(LAST_EXPORT_COOKIE_NAME); @@ -48,6 +59,12 @@ const CourseExportPage = ({ intl, courseId }) => { } }, []); + if (!hasExportPermissions) { + return ( + + ); + } + return ( <> @@ -89,13 +106,14 @@ const CourseExportPage = ({ intl, courseId }) => { className="mb-4" onClick={() => dispatch(startExportingCourse(courseId))} iconBefore={ArrowCircleDownIcon} + disabled={viewOnly} > {intl.formatMessage(messages.buttonTitle)} )} - {exportTriggered && } + {exportTriggered && } diff --git a/src/export-page/CourseExportPage.test.jsx b/src/export-page/CourseExportPage.test.jsx index 05db07d5b..6844d86ba 100644 --- a/src/export-page/CourseExportPage.test.jsx +++ b/src/export-page/CourseExportPage.test.jsx @@ -12,6 +12,9 @@ import initializeStore from '../store'; import stepperMessages from './export-stepper/messages'; import modalErrorMessages from './export-modal-error/messages'; import { getExportStatusApiUrl, postExportCourseApiUrl } from './data/api'; +import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api'; +import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks'; +import { executeThunk } from '../utils'; import { EXPORT_STAGES } from './data/constants'; import { exportPageMock } from './__mocks__'; import messages from './messages'; @@ -22,6 +25,8 @@ let axiosMock; let cookies; const courseId = '123'; const courseName = 'About Node JS'; +const userId = 3; +let userPermissionsData = { permissions: [] }; jest.mock('../generic/model-store', () => ({ useModel: jest.fn().mockReturnValue({ @@ -49,7 +54,7 @@ describe('', () => { beforeEach(() => { initializeMockApp({ authenticatedUser: { - userId: 3, + userId, username: 'abc123', administrator: true, roles: [], @@ -60,6 +65,14 @@ describe('', () => { axiosMock .onGet(postExportCourseApiUrl(courseId)) .reply(200, exportPageMock); + axiosMock + .onGet(getUserPermissionsEnabledFlagUrl) + .reply(200, { enabled: false }); + axiosMock + .onGet(getUserPermissionsUrl(courseId, userId)) + .reply(200, userPermissionsData); + executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch); + executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch); cookies = new Cookies(); cookies.get.mockReturnValue(null); }); @@ -85,8 +98,57 @@ describe('', () => { expect(getByText(messages.buttonTitle.defaultMessage)).toBeInTheDocument(); }); }); + 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 render without errors if correct permissions', async () => { + const { getByText } = render(); + userPermissionsData = { permissions: ['manage_course_settings'] }; + 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); + await waitFor(() => { + expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument(); + const exportPageElement = getByText(messages.headingTitle.defaultMessage, { + selector: 'h2.sub-header-title', + }); + expect(exportPageElement).toBeInTheDocument(); + expect(getByText(messages.titleUnderButton.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.description2.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.buttonTitle.defaultMessage)).toBeInTheDocument(); + }); + }); + it('should render without errors if viewOnly permissions', async () => { + const { getByText } = render(); + userPermissionsData = { permissions: ['view_course_settings'] }; + 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); + await waitFor(() => { + expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument(); + const exportPageElement = getByText(messages.headingTitle.defaultMessage, { + selector: 'h2.sub-header-title', + }); + const buttonElement = getByText(messages.buttonTitle.defaultMessage); + expect(exportPageElement).toBeInTheDocument(); + expect(getByText(messages.titleUnderButton.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.description2.defaultMessage)).toBeInTheDocument(); + expect(buttonElement).toBeInTheDocument(); + expect(buttonElement.disabled).toEqual(true); + }); + userPermissionsData = { permissions: ['manage_course_settings'] }; + axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, userPermissionsData); + await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch); + }); it('should start exporting on click', async () => { const { getByText, container } = render(); + axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true }); + await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch); const button = container.querySelector('.btn-primary'); fireEvent.click(button); expect(getByText(stepperMessages.stepperPreparingDescription.defaultMessage)).toBeInTheDocument(); @@ -96,6 +158,8 @@ describe('', () => { .onGet(getExportStatusApiUrl(courseId)) .reply(200, { exportStatus: EXPORT_STAGES.EXPORTING, exportError: { rawErrorMsg: 'test error', editUnitUrl: 'http://test-url.test' } }); const { getByText, queryByText, container } = render(); + axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true }); + await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch); const startExportButton = container.querySelector('.btn-primary'); fireEvent.click(startExportButton); // eslint-disable-next-line no-promise-executor-return @@ -116,6 +180,8 @@ describe('', () => { .onGet(getExportStatusApiUrl(courseId)) .reply(200, { exportStatus: EXPORT_STAGES.SUCCESS, exportOutput: '/test-download-path.test' }); const { getByText, container } = render(); + axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true }); + await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch); const startExportButton = container.querySelector('.btn-primary'); fireEvent.click(startExportButton); // eslint-disable-next-line no-promise-executor-return @@ -129,6 +195,8 @@ describe('', () => { .onGet(getExportStatusApiUrl(courseId)) .reply(200, { exportStatus: EXPORT_STAGES.SUCCESS, exportOutput: 'http://test-download-path.test' }); const { getByText, container } = render(); + axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true }); + await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch); const startExportButton = container.querySelector('.btn-primary'); fireEvent.click(startExportButton); // eslint-disable-next-line no-promise-executor-return diff --git a/src/export-page/export-stepper/ExportStepper.jsx b/src/export-page/export-stepper/ExportStepper.jsx index 0fbbe66e5..860f59e69 100644 --- a/src/export-page/export-stepper/ExportStepper.jsx +++ b/src/export-page/export-stepper/ExportStepper.jsx @@ -17,7 +17,7 @@ import { EXPORT_STAGES } from '../data/constants'; import { RequestStatus } from '../../data/constants'; import messages from './messages'; -const ExportStepper = ({ intl, courseId }) => { +const ExportStepper = ({ intl, courseId, viewOnly }) => { const currentStage = useSelector(getCurrentStage); const downloadPath = useSelector(getDownloadPath); const successDate = useSelector(getSuccessDate); @@ -90,7 +90,7 @@ const ExportStepper = ({ intl, courseId }) => { errorMessage={errorMessage} hasError={!!errorMessage} /> - {downloadPath && currentStage === EXPORT_STAGES.SUCCESS && } + {downloadPath && currentStage === EXPORT_STAGES.SUCCESS && } ); }; @@ -98,6 +98,7 @@ const ExportStepper = ({ intl, courseId }) => { ExportStepper.propTypes = { intl: intlShape.isRequired, courseId: PropTypes.string.isRequired, + viewOnly: PropTypes.bool.isRequired, }; export default injectIntl(ExportStepper); diff --git a/src/export-page/export-stepper/ExportStepper.test.jsx b/src/export-page/export-stepper/ExportStepper.test.jsx index c8ca1e485..d3d0c1cac 100644 --- a/src/export-page/export-stepper/ExportStepper.test.jsx +++ b/src/export-page/export-stepper/ExportStepper.test.jsx @@ -1,20 +1,36 @@ import React from 'react'; import { render } from '@testing-library/react'; +import MockAdapter from 'axios-mock-adapter'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { initializeMockApp } from '@edx/frontend-platform'; import { AppProvider } from '@edx/frontend-platform/react'; import initializeStore from '../../store'; +import { executeThunk } from '../../utils'; + import messages from './messages'; import ExportStepper from './ExportStepper'; +import { exportStepperPageMock } from './__mocks__'; +import { fetchExportStatus } from '../data/thunks'; +import { getExportStatusApiUrl } from '../data/api'; const courseId = 'course-123'; +let axiosMock; let store; const RootWrapper = () => ( - + + + +); + +const ViewOnlyRootWrapper = () => ( + + + ); @@ -30,9 +46,22 @@ describe('', () => { }, }); store = initializeStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock + .onGet(getExportStatusApiUrl(courseId)) + .reply(200, exportStepperPageMock); + executeThunk(fetchExportStatus(courseId), store.dispatch); }); it('render stepper correctly', () => { const { getByText } = render(); expect(getByText(messages.stepperHeaderTitle.defaultMessage)).toBeInTheDocument(); }); + it('render stepper correctly if button is disabled', () => { + const { getByText } = render(); + expect(getByText(messages.stepperHeaderTitle.defaultMessage)).toBeInTheDocument(); + const buttonElement = getByText(messages.downloadCourseButtonTitle.defaultMessage, { + selector: '.disabled', + }); + expect(buttonElement).toBeInTheDocument(); + }); }); diff --git a/src/export-page/export-stepper/__mocks__/exportStepperPage.js b/src/export-page/export-stepper/__mocks__/exportStepperPage.js new file mode 100644 index 000000000..bea614c8f --- /dev/null +++ b/src/export-page/export-stepper/__mocks__/exportStepperPage.js @@ -0,0 +1,4 @@ +module.exports = { + exportStatus: 3, + exportOutput: '/test', +}; diff --git a/src/export-page/export-stepper/__mocks__/index.js b/src/export-page/export-stepper/__mocks__/index.js new file mode 100644 index 000000000..041e028e5 --- /dev/null +++ b/src/export-page/export-stepper/__mocks__/index.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as exportStepperPageMock } from './exportStepperPage'; diff --git a/src/header/Header.jsx b/src/header/Header.jsx index 2b7d96f07..f3f832722 100644 --- a/src/header/Header.jsx +++ b/src/header/Header.jsx @@ -28,7 +28,7 @@ const Header = ({ const hasSettingsPermissions = !userPermissionsEnabled || (userPermissionsEnabled && (checkPermission('manage_advanced_settings') || checkPermission('view_course_settings'))); const hasToolsPermissions = !userPermissionsEnabled - || (userPermissionsEnabled && (checkPermission('manage_course_settings') || checkPermission('view_course_settings'))); + || (userPermissionsEnabled && (checkPermission('manage_course_settings') || checkPermission('view_course_settings'))); const studioBaseUrl = getConfig().STUDIO_BASE_URL; const contentMenu = getContentMenuItems({ studioBaseUrl, @@ -37,6 +37,12 @@ const Header = ({ hasContentPermissions, }); const mainMenuDropdowns = []; + const toolsMenu = getToolsMenuItems({ + studioBaseUrl, + courseId, + intl, + hasToolsPermissions, + }); useEffect(() => { dispatch(fetchUserPermissionsEnabledFlag()); @@ -65,17 +71,16 @@ const Header = ({ hasSettingsPermissions, }), }, - { - id: `${intl.formatMessage(messages['header.links.tools'])}-dropdown-menu`, - buttonTitle: intl.formatMessage(messages['header.links.tools']), - items: getToolsMenuItems({ - studioBaseUrl, - courseId, - intl, - hasToolsPermissions, - }), - }, ); + if (toolsMenu.length > 0) { + mainMenuDropdowns.push( + { + id: `${intl.formatMessage(messages['header.links.tools'])}-dropdown-menu`, + buttonTitle: intl.formatMessage(messages['header.links.tools']), + items: toolsMenu, + }, + ); + } const outlineLink = `${studioBaseUrl}/course/${courseId}`; return ( diff --git a/src/header/utils.js b/src/header/utils.js index 524bacd11..26c3929b0 100644 --- a/src/header/utils.js +++ b/src/header/utils.js @@ -91,18 +91,16 @@ export const getToolsMenuItems = ({ hasToolsPermissions, }) => { const items = []; - items.push( - { - href: `${studioBaseUrl}/import/${courseId}`, - title: intl.formatMessage(messages['header.links.import']), - }, - { - href: `${studioBaseUrl}/export/${courseId}`, - title: intl.formatMessage(messages['header.links.export']), - }, - ); if (hasToolsPermissions) { items.push( + { + href: `${studioBaseUrl}/import/${courseId}`, + title: intl.formatMessage(messages['header.links.import']), + }, + { + href: `${studioBaseUrl}/export/${courseId}`, + title: intl.formatMessage(messages['header.links.export']), + }, { href: `${studioBaseUrl}/checklists/${courseId}`, title: intl.formatMessage(messages['header.links.checklists']), diff --git a/src/header/utils.test.js b/src/header/utils.test.js index 5eab759d5..e684affb0 100644 --- a/src/header/utils.test.js +++ b/src/header/utils.test.js @@ -51,9 +51,9 @@ describe('header utils', () => { const actualItems = getToolsMenuItems(toolsProps); expect(actualItems).toHaveLength(3); }); - it('should not include Checklist option', () => { + it('should not include any items if there are no permissions', () => { const actualItems = getToolsMenuItems({ ...baseProps, hasToolsPermissions: false }); - expect(actualItems).toHaveLength(2); + expect(actualItems).toHaveLength(0); }); }); }); diff --git a/src/import-page/CourseImportPage.jsx b/src/import-page/CourseImportPage.jsx index 5154f6a5c..9f1cd95bc 100644 --- a/src/import-page/CourseImportPage.jsx +++ b/src/import-page/CourseImportPage.jsx @@ -22,6 +22,9 @@ import { LAST_IMPORT_COOKIE_NAME } from './data/constants'; import ImportSidebar from './import-sidebar/ImportSidebar'; import FileSection from './file-section/FileSection'; import messages from './messages'; +import { useUserPermissions } from '../generic/hooks'; +import { getUserPermissionsEnabled } from '../generic/data/selectors'; +import PermissionDeniedAlert from '../generic/PermissionDeniedAlert'; const CourseImportPage = ({ intl, courseId }) => { const dispatch = useDispatch(); @@ -32,6 +35,14 @@ const CourseImportPage = ({ intl, courseId }) => { const loadingStatus = useSelector(getLoadingStatus); const anyRequestFailed = savingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.FAILED; const anyRequestInProgress = savingStatus === RequestStatus.PENDING || loadingStatus === RequestStatus.IN_PROGRESS; + const { checkPermission } = useUserPermissions(); + const userPermissionsEnabled = useSelector(getUserPermissionsEnabled); + const hasImportPermissions = !userPermissionsEnabled || ( + userPermissionsEnabled && (checkPermission('manage_course_settings') || checkPermission('view_course_settings')) + ); + const viewOnly = !userPermissionsEnabled || ( + userPermissionsEnabled && checkPermission('view_course_settings') && !checkPermission('manage_course_settings') + ); useEffect(() => { const cookieData = cookies.get(LAST_IMPORT_COOKIE_NAME); @@ -43,6 +54,12 @@ const CourseImportPage = ({ intl, courseId }) => { } }, []); + if (!hasImportPermissions) { + return ( + + ); + } + return ( <> @@ -72,7 +89,7 @@ const CourseImportPage = ({ intl, courseId }) => {

{intl.formatMessage(messages.description1)}

{intl.formatMessage(messages.description2)}

{intl.formatMessage(messages.description3)}

- + {importTriggered && } diff --git a/src/import-page/CourseImportPage.test.jsx b/src/import-page/CourseImportPage.test.jsx index 835931269..ccdf30db4 100644 --- a/src/import-page/CourseImportPage.test.jsx +++ b/src/import-page/CourseImportPage.test.jsx @@ -14,12 +14,17 @@ import CourseImportPage from './CourseImportPage'; import { getImportStatusApiUrl } from './data/api'; import { IMPORT_STAGES } from './data/constants'; import stepperMessages from './import-stepper/messages'; +import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api'; +import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks'; +import { executeThunk } from '../utils'; let store; let axiosMock; let cookies; const courseId = '123'; const courseName = 'About Node JS'; +const userId = 3; +let userPermissionsData = { permissions: [] }; jest.mock('../generic/model-store', () => ({ useModel: jest.fn().mockReturnValue({ @@ -47,7 +52,7 @@ describe('', () => { beforeEach(() => { initializeMockApp({ authenticatedUser: { - userId: 3, + userId, username: 'abc123', administrator: true, roles: [], @@ -58,6 +63,12 @@ describe('', () => { axiosMock .onGet(getImportStatusApiUrl(courseId, 'testFileName.test')) .reply(200, { importStatus: 1, message: '' }); + axiosMock + .onGet(getUserPermissionsEnabledFlagUrl) + .reply(200, { enabled: false }); + axiosMock + .onGet(getUserPermissionsUrl(courseId, userId)) + .reply(200, userPermissionsData); cookies = new Cookies(); cookies.get.mockReturnValue(null); }); @@ -83,6 +94,30 @@ describe('', () => { expect(getByText(messages.description3.defaultMessage)).toBeInTheDocument(); }); }); + 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 render without errors if correct permissions', async () => { + const { getByText } = render(); + userPermissionsData = { permissions: ['manage_course_settings'] }; + 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); + await waitFor(() => { + expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument(); + const importPageElement = getByText(messages.headingTitle.defaultMessage, { + selector: 'h2.sub-header-title', + }); + expect(importPageElement).toBeInTheDocument(); + expect(getByText(messages.description1.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.description2.defaultMessage)).toBeInTheDocument(); + expect(getByText(messages.description3.defaultMessage)).toBeInTheDocument(); + }); + }); it('should fetch status without clicking when cookies has', async () => { cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.test' }); const { getByText } = render(); diff --git a/src/import-page/file-section/FileSection.jsx b/src/import-page/file-section/FileSection.jsx index 012023f04..3a495f3cd 100644 --- a/src/import-page/file-section/FileSection.jsx +++ b/src/import-page/file-section/FileSection.jsx @@ -1,12 +1,12 @@ import React from 'react'; import { + FormattedMessage, injectIntl, intlShape, } from '@edx/frontend-platform/i18n'; import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; -import { Card, Dropzone } from '@edx/paragon'; - +import { Alert, Card, Dropzone } from '@edx/paragon'; import { IMPORT_STAGES } from '../data/constants'; import { getCurrentStage, getError, getFileName, getImportTriggered, @@ -14,7 +14,7 @@ import { import messages from './messages'; import { handleProcessUpload } from '../data/thunks'; -const FileSection = ({ intl, courseId }) => { +const FileSection = ({ intl, courseId, viewOnly }) => { const dispatch = useDispatch(); const importTriggered = useSelector(getImportTriggered); const currentStage = useSelector(getCurrentStage); @@ -30,21 +30,25 @@ const FileSection = ({ intl, courseId }) => { subtitle={fileName && intl.formatMessage(messages.fileChosen, { fileName })} /> - {isShowedDropzone - && ( - dispatch(handleProcessUpload( - courseId, - fileData, - requestConfig, - handleError, - )) - } - accept={{ 'application/gzip': ['.tar.gz'] }} - data-testid="dropzone" - /> - )} + {!viewOnly && isShowedDropzone && ( + dispatch(handleProcessUpload( + courseId, + fileData, + requestConfig, + handleError, + )) + } + accept={{ 'application/gzip': ['.tar.gz'] }} + data-testid="dropzone" + /> + )} + {viewOnly && ( + + + + )} ); @@ -53,6 +57,7 @@ const FileSection = ({ intl, courseId }) => { FileSection.propTypes = { intl: intlShape.isRequired, courseId: PropTypes.string.isRequired, + viewOnly: PropTypes.bool.isRequired, }; export default injectIntl(FileSection); diff --git a/src/import-page/file-section/FileSection.test.jsx b/src/import-page/file-section/FileSection.test.jsx index 6d6bfb2d2..f6c1b1e15 100644 --- a/src/import-page/file-section/FileSection.test.jsx +++ b/src/import-page/file-section/FileSection.test.jsx @@ -14,7 +14,15 @@ const courseId = '123'; const RootWrapper = () => ( - + + + +); + +const RootWrapperViewOnly = () => ( + + + ); @@ -27,6 +35,7 @@ describe('', () => { username: 'abc123', administrator: true, roles: [], + permisions: [], }, }); store = initializeStore(); @@ -43,6 +52,13 @@ describe('', () => { expect(getByTestId('dropzone')).toBeInTheDocument(); }); }); + it('should not render Dropzone when view is viewOnly', async () => { + const { getByText, queryByTestId, container } = render(); + await waitFor(() => { + expect(queryByTestId(container, 'dropzone')).not.toBeInTheDocument(); + expect(getByText(messages.viewOnlyAlert.defaultMessage)).toBeInTheDocument(); + }); + }); it('should work Dropzone', async () => { const { getByText, getByTestId, queryByTestId, container, diff --git a/src/import-page/file-section/messages.js b/src/import-page/file-section/messages.js index 6ad57b9a5..ecfb6939e 100644 --- a/src/import-page/file-section/messages.js +++ b/src/import-page/file-section/messages.js @@ -9,6 +9,10 @@ const messages = defineMessages({ id: 'course-authoring.import.file-section.chosen-file', defaultMessage: 'File chosen: {fileName}', }, + viewOnlyAlert: { + id: 'course-authoring.import.file-section.view-only-alert', + defaultMessage: 'You have view only access to this page. If you feel you should have full access, please reach out to your course team admin to be given access.', + }, }); export default messages; diff --git a/src/pages-and-resources/PagesAndResources.test.jsx b/src/pages-and-resources/PagesAndResources.test.jsx index b30aedaaf..f97e60a61 100644 --- a/src/pages-and-resources/PagesAndResources.test.jsx +++ b/src/pages-and-resources/PagesAndResources.test.jsx @@ -8,6 +8,12 @@ import * as xpertUnitSummaryApi from './xpert-unit-summary/data/api'; const courseId = 'course-v1:edX+TestX+Test_Course'; +jest.mock('../generic/hooks', () => ({ + useUserPermissions: jest.fn(() => ({ + checkPermission: jest.fn(() => true), + })), +})); + describe('PagesAndResources', () => { beforeEach(() => { jest.clearAllMocks();