Compare commits
7 Commits
test_hyper
...
CourseRole
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2bfb1fb7b | ||
|
|
c754a5e519 | ||
|
|
1e9146a5b9 | ||
|
|
a518fada29 | ||
|
|
69d9ea318e | ||
|
|
e74e1ff5aa | ||
|
|
1137dae97a |
@@ -0,0 +1,21 @@
|
|||||||
|
Background
|
||||||
|
==========
|
||||||
|
|
||||||
|
This is a summary of the technical decisions made for the Roles & Permissions
|
||||||
|
project as we implemented the permissions check system in the ``frontend-app-course-authoring``.
|
||||||
|
|
||||||
|
The ``frontend-app-course-authoring`` was already created when the
|
||||||
|
Permissions project started, so it already had a coding style, store
|
||||||
|
management and its own best practices.
|
||||||
|
We aligned to these requirements.
|
||||||
|
|
||||||
|
Frontend Architecture
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
* `Readme <https://github.com/openedx/frontend-app-course-authoring#readme>`__
|
||||||
|
* Developing locally:
|
||||||
|
https://github.com/openedx/frontend-app-course-authoring#readme
|
||||||
|
* **React.js** application ``version: 17.0.2``
|
||||||
|
* **Redux** store management ``version: 4.0.5``
|
||||||
|
* It uses **Thunk** for adding to Redux the ability of returning
|
||||||
|
functions.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
Local Development & Testing
|
||||||
|
===========================
|
||||||
|
|
||||||
|
Backend
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
The backend endpoints lives in the ``edx-platform`` repo, specifically
|
||||||
|
in this file: ``openedx/core/djangoapps/course_roles/views.py``
|
||||||
|
|
||||||
|
For quickly testing the different permissions and the flag change you
|
||||||
|
can tweak the values directly in the above file.
|
||||||
|
|
||||||
|
* ``UserPermissionsView`` is in charge of returning the permissions, so
|
||||||
|
for sending the permissions you want to check, you could do something
|
||||||
|
like this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
permissions = {
|
||||||
|
'user_id': user_id,
|
||||||
|
'course_key': str(course_key),
|
||||||
|
#'permissions': sorted(permission.value.name for permission in permissions_set),
|
||||||
|
'permissions': ['the_permissions_being_tested']
|
||||||
|
}
|
||||||
|
return Response(permissions)
|
||||||
|
|
||||||
|
By making this change, the permissions object will be bypassed and
|
||||||
|
send a plain array with the specific permissions being tested.
|
||||||
|
|
||||||
|
|
||||||
|
* ``UserPermissionsFlagView`` is in charge of returning the flag value
|
||||||
|
(boolean), so you can easily turn the boolean like this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
#payload = {'enabled': use_permission_checks()}
|
||||||
|
payload = {'enabled': true}
|
||||||
|
return Response(payload)
|
||||||
|
|
||||||
|
Flags
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
You’ll need at least 2 flags to start:
|
||||||
|
|
||||||
|
* The basic flag for enabling the backend permissions system: ``course_roles.use_permission_checks``.
|
||||||
|
|
||||||
|
* The flag for enabling the page you want to test, for instance Course Team: ``contentstore.new_studio_mfe.use_new_course_team_page``.
|
||||||
|
|
||||||
|
All flags for enabling pages in the Studio MFE are listed
|
||||||
|
`here <https://2u-internal.atlassian.net/wiki/x/CQCcHQ>`__.
|
||||||
|
|
||||||
|
Flags can be added by:
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
* Enter to ``http://localhost:18000/admin/``.
|
||||||
|
* Log in as an admin.
|
||||||
|
* Go to ``http://localhost:18000/admin/waffle/flag/``.
|
||||||
|
* Click on ``+ADD FLAG`` button at the top right of the page and add
|
||||||
|
the flag you need.
|
||||||
|
|
||||||
|
Testing
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
For unit testing you run the npm script included in the ``package.json``, you can use it plainly for testing all components at once: ``npm run test``.
|
||||||
|
|
||||||
|
Or you can test one file at a time: ``npm run test path-to-file``.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
Permissions Check implementation
|
||||||
|
================================
|
||||||
|
|
||||||
|
For the permissions checks we basically hit 2 endpoints from the
|
||||||
|
``edx-platform`` repo:
|
||||||
|
|
||||||
|
* **Permissions**:
|
||||||
|
``/api/course_roles/v1/user_permissions/?course_id=[course_key]&user_id=[user_id]``
|
||||||
|
|
||||||
|
Which will return this structure:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
permissions = {
|
||||||
|
'user_id': [user_id],
|
||||||
|
'course_key': [course_key],
|
||||||
|
'permissions': ['permission_1', 'permission_2']
|
||||||
|
}
|
||||||
|
|
||||||
|
* **Permissions enabled** (which returns the boolean flag value): ``/api/course_roles/v1/user_permissions/enabled/``
|
||||||
|
|
||||||
|
The basic scaffolding for *fetching* and *storing* the permissions is located in the ``src/generic/data`` folder:
|
||||||
|
|
||||||
|
* ``api.js``: Exposes the ``getUserPermissions(courseId)`` and ``getUserPermissionsEnabledFlag()`` methods.
|
||||||
|
* ``selectors.js``: Exposes the selectors ``getUserPermissions`` and ``getUserPermissionsEnabled`` to be used by ``useSelector()``.
|
||||||
|
* ``slice.js``: Exposes the ``updateUserPermissions`` and ``updateUserPermissionsEnabled`` methods that will be used by the ``thunks.js`` file for dispatching and storing.
|
||||||
|
* ``thunks.js``: Exposes the ``fetchUserPermissionsQuery(courseId)`` and ``fetchUserPermissionsEnabledFlag()`` methods for fetching.
|
||||||
|
|
||||||
|
In the ``src/generic/hooks.jsx`` we created a custom hook for exposing the ``checkPermission`` method, so that way we can call
|
||||||
|
this method from any page and pass the permission we want to check for the current logged in user.
|
||||||
|
|
||||||
|
In this example on the ``src/course-team/CourseTeam.jsx`` page, we use the hook for checking if the current user has the ``manage_all_users``
|
||||||
|
permission:
|
||||||
|
|
||||||
|
1. First, we import the hook (line 1).
|
||||||
|
|
||||||
|
2. Then we call the ``checkPermission`` method and assign it to a const (line 2).
|
||||||
|
|
||||||
|
3. Finally we use the const for showing or hiding a button (line 8).
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
1. import { useUserPermissions } from '../generic/hooks';
|
||||||
|
2. const hasManageAllUsersPerm = checkPermission('manage_all_users');
|
||||||
|
|
||||||
|
3. <SubHeader
|
||||||
|
4. title={intl.formatMessage(messages.headingTitle)}
|
||||||
|
5. subtitle={intl.formatMessage(messages.headingSubtitle)}
|
||||||
|
6. headerActions={(
|
||||||
|
7. isAllowActions ||
|
||||||
|
8. hasManageAllUsersPerm
|
||||||
|
9. ) && (
|
||||||
|
10. <Button
|
||||||
|
11. variant="primary"
|
||||||
|
12. iconBefore={IconAdd}
|
||||||
|
13. size="sm"
|
||||||
|
14. onClick={openForm}
|
||||||
|
15. >
|
||||||
|
16. {intl.formatMessage(messages.addNewMemberButton)}
|
||||||
|
17. </Button>
|
||||||
|
18. )}
|
||||||
|
19. />
|
||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -65,7 +65,7 @@
|
|||||||
"@testing-library/user-event": "^13.2.1",
|
"@testing-library/user-event": "^13.2.1",
|
||||||
"axios-mock-adapter": "1.22.0",
|
"axios-mock-adapter": "1.22.0",
|
||||||
"glob": "7.2.3",
|
"glob": "7.2.3",
|
||||||
"husky": "7.0.4",
|
"husky": "^7.0.4",
|
||||||
"jest-canvas-mock": "^2.5.2",
|
"jest-canvas-mock": "^2.5.2",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"react-test-renderer": "17.0.2",
|
"react-test-renderer": "17.0.2",
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
"@testing-library/user-event": "^13.2.1",
|
"@testing-library/user-event": "^13.2.1",
|
||||||
"axios-mock-adapter": "1.22.0",
|
"axios-mock-adapter": "1.22.0",
|
||||||
"glob": "7.2.3",
|
"glob": "7.2.3",
|
||||||
"husky": "7.0.4",
|
"husky": "^7.0.4",
|
||||||
"jest-canvas-mock": "^2.5.2",
|
"jest-canvas-mock": "^2.5.2",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"react-test-renderer": "17.0.2",
|
"react-test-renderer": "17.0.2",
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import PermissionDeniedAlert from './generic/PermissionDeniedAlert';
|
|||||||
import { getCourseAppsApiStatus } from './pages-and-resources/data/selectors';
|
import { getCourseAppsApiStatus } from './pages-and-resources/data/selectors';
|
||||||
import { RequestStatus } from './data/constants';
|
import { RequestStatus } from './data/constants';
|
||||||
import Loading from './generic/Loading';
|
import Loading from './generic/Loading';
|
||||||
|
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from './generic/data/thunks';
|
||||||
|
import { getUserPermissions } from './generic/data/selectors';
|
||||||
|
|
||||||
const AppHeader = ({
|
const AppHeader = ({
|
||||||
courseNumber, courseOrg, courseTitle, courseId,
|
courseNumber, courseOrg, courseTitle, courseId,
|
||||||
@@ -40,9 +42,14 @@ AppHeader.defaultProps = {
|
|||||||
|
|
||||||
const CourseAuthoringPage = ({ courseId, children }) => {
|
const CourseAuthoringPage = ({ courseId, children }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const userPermissions = useSelector(getUserPermissions);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchCourseDetail(courseId));
|
dispatch(fetchCourseDetail(courseId));
|
||||||
|
dispatch(fetchUserPermissionsEnabledFlag());
|
||||||
|
if (!userPermissions) {
|
||||||
|
dispatch(fetchUserPermissionsQuery(courseId));
|
||||||
|
}
|
||||||
}, [courseId]);
|
}, [courseId]);
|
||||||
|
|
||||||
const courseDetail = useModel('courseDetails', courseId);
|
const courseDetail = useModel('courseDetails', courseId);
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ import validateAdvancedSettingsData from './utils';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import ModalError from './modal-error/ModalError';
|
import ModalError from './modal-error/ModalError';
|
||||||
import getPageHeadTitle from '../generic/utils';
|
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 AdvancedSettings = ({ intl, courseId }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -41,6 +44,13 @@ const AdvancedSettings = ({ intl, courseId }) => {
|
|||||||
const courseDetails = useModel('courseDetails', courseId);
|
const courseDetails = useModel('courseDetails', courseId);
|
||||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
|
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(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchCourseAppSettings(courseId));
|
dispatch(fetchCourseAppSettings(courseId));
|
||||||
dispatch(fetchProctoringExamErrors(courseId));
|
dispatch(fetchProctoringExamErrors(courseId));
|
||||||
@@ -83,6 +93,11 @@ const AdvancedSettings = ({ intl, courseId }) => {
|
|||||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
if (showPermissionDeniedAlert) {
|
||||||
|
return (
|
||||||
|
<PermissionDeniedAlert />
|
||||||
|
);
|
||||||
|
}
|
||||||
if (loadingSettingsStatus === RequestStatus.DENIED) {
|
if (loadingSettingsStatus === RequestStatus.DENIED) {
|
||||||
return (
|
return (
|
||||||
<div className="row justify-content-center m-6">
|
<div className="row justify-content-center m-6">
|
||||||
@@ -215,6 +230,7 @@ const AdvancedSettings = ({ intl, courseId }) => {
|
|||||||
handleBlur={handleSettingBlur}
|
handleBlur={handleSettingBlur}
|
||||||
isEditableState={isEditableState}
|
isEditableState={isEditableState}
|
||||||
setIsEditableState={setIsEditableState}
|
setIsEditableState={setIsEditableState}
|
||||||
|
disableForm={viewOnly}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { initializeMockApp } from '@edx/frontend-platform';
|
|||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { AppProvider } from '@edx/frontend-platform/react';
|
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 MockAdapter from 'axios-mock-adapter';
|
||||||
|
|
||||||
import initializeStore from '../store';
|
import initializeStore from '../store';
|
||||||
@@ -13,11 +17,15 @@ import { getCourseAdvancedSettingsApiUrl } from './data/api';
|
|||||||
import { updateCourseAppSetting } from './data/thunks';
|
import { updateCourseAppSetting } from './data/thunks';
|
||||||
import AdvancedSettings from './AdvancedSettings';
|
import AdvancedSettings from './AdvancedSettings';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api';
|
||||||
|
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
|
||||||
|
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
let store;
|
let store;
|
||||||
const mockPathname = '/foo-bar';
|
const mockPathname = '/foo-bar';
|
||||||
const courseId = '123';
|
const courseId = '123';
|
||||||
|
const userId = 3;
|
||||||
|
const userPermissionsData = { permissions: ['view_course_settings', 'manage_advanced_settings'] };
|
||||||
|
|
||||||
// Mock the TextareaAutosize component
|
// Mock the TextareaAutosize component
|
||||||
jest.mock('react-textarea-autosize', () => jest.fn((props) => (
|
jest.mock('react-textarea-autosize', () => jest.fn((props) => (
|
||||||
@@ -43,11 +51,23 @@ const RootWrapper = () => (
|
|||||||
</AppProvider>
|
</AppProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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('<AdvancedSettings />', () => {
|
describe('<AdvancedSettings />', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
initializeMockApp({
|
initializeMockApp({
|
||||||
authenticatedUser: {
|
authenticatedUser: {
|
||||||
userId: 3,
|
userId,
|
||||||
username: 'abc123',
|
username: 'abc123',
|
||||||
administrator: true,
|
administrator: true,
|
||||||
roles: [],
|
roles: [],
|
||||||
@@ -58,7 +78,9 @@ describe('<AdvancedSettings />', () => {
|
|||||||
axiosMock
|
axiosMock
|
||||||
.onGet(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`)
|
.onGet(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`)
|
||||||
.reply(200, advancedSettingsMock);
|
.reply(200, advancedSettingsMock);
|
||||||
|
permissionsMockStore(userPermissionsData);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render without errors', async () => {
|
it('should render without errors', async () => {
|
||||||
const { getByText } = render(<RootWrapper />);
|
const { getByText } = render(<RootWrapper />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -161,4 +183,29 @@ describe('<AdvancedSettings />', () => {
|
|||||||
await executeThunk(updateCourseAppSetting(courseId, [3, 2, 1]), store.dispatch);
|
await executeThunk(updateCourseAppSetting(courseId, [3, 2, 1]), store.dispatch);
|
||||||
expect(getByText('Your policy changes have been saved.')).toBeInTheDocument();
|
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(<RootWrapper />);
|
||||||
|
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(<RootWrapper />);
|
||||||
|
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(<RootWrapper />);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByLabelText('Advanced Module List')).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const SettingCard = ({
|
|||||||
setIsEditableState,
|
setIsEditableState,
|
||||||
// injected
|
// injected
|
||||||
intl,
|
intl,
|
||||||
|
disableForm,
|
||||||
}) => {
|
}) => {
|
||||||
const { deprecated, help, displayName } = settingData;
|
const { deprecated, help, displayName } = settingData;
|
||||||
const initialValue = JSON.stringify(settingData.value, null, 4);
|
const initialValue = JSON.stringify(settingData.value, null, 4);
|
||||||
@@ -100,6 +101,7 @@ const SettingCard = ({
|
|||||||
onChange={handleSettingChange}
|
onChange={handleSettingChange}
|
||||||
aria-label={displayName}
|
aria-label={displayName}
|
||||||
onBlur={handleCardBlur}
|
onBlur={handleCardBlur}
|
||||||
|
disabled={disableForm}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
@@ -135,6 +137,7 @@ SettingCard.propTypes = {
|
|||||||
saveSettingsPrompt: PropTypes.bool.isRequired,
|
saveSettingsPrompt: PropTypes.bool.isRequired,
|
||||||
isEditableState: PropTypes.bool.isRequired,
|
isEditableState: PropTypes.bool.isRequired,
|
||||||
setIsEditableState: PropTypes.func.isRequired,
|
setIsEditableState: PropTypes.func.isRequired,
|
||||||
|
disableForm: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(SettingCard);
|
export default injectIntl(SettingCard);
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ import DeleteModal from './delete-modal/DeleteModal';
|
|||||||
import PageAlerts from './page-alerts/PageAlerts';
|
import PageAlerts from './page-alerts/PageAlerts';
|
||||||
import { useCourseOutline } from './hooks';
|
import { useCourseOutline } from './hooks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import { useUserPermissions } from '../generic/hooks';
|
||||||
|
import { getUserPermissionsEnabled } from '../generic/data/selectors';
|
||||||
|
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
|
||||||
|
|
||||||
const CourseOutline = ({ courseId }) => {
|
const CourseOutline = ({ courseId }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -108,6 +111,12 @@ const CourseOutline = ({ courseId }) => {
|
|||||||
|
|
||||||
const [sections, setSections] = useState(sectionsList);
|
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];
|
let initialSections = [...sectionsList];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -241,6 +250,12 @@ const CourseOutline = ({ courseId }) => {
|
|||||||
setSections(sectionsList);
|
setSections(sectionsList);
|
||||||
}, [sectionsList]);
|
}, [sectionsList]);
|
||||||
|
|
||||||
|
if (!hasOutlinePermissions) {
|
||||||
|
return (
|
||||||
|
<PermissionDeniedAlert />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -48,10 +48,15 @@ import pasteButtonMessages from './paste-button/messages';
|
|||||||
import subsectionMessages from './subsection-card/messages';
|
import subsectionMessages from './subsection-card/messages';
|
||||||
import pageAlertMessages from './page-alerts/messages';
|
import pageAlertMessages from './page-alerts/messages';
|
||||||
|
|
||||||
|
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api';
|
||||||
|
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
|
||||||
|
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
let store;
|
let store;
|
||||||
const mockPathname = '/foo-bar';
|
const mockPathname = '/foo-bar';
|
||||||
const courseId = '123';
|
const courseId = '123';
|
||||||
|
const userId = 3;
|
||||||
|
const userPermissionsData = { permissions: [] };
|
||||||
|
|
||||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||||
|
|
||||||
@@ -90,7 +95,7 @@ describe('<CourseOutline />', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
initializeMockApp({
|
initializeMockApp({
|
||||||
authenticatedUser: {
|
authenticatedUser: {
|
||||||
userId: 3,
|
userId,
|
||||||
username: 'abc123',
|
username: 'abc123',
|
||||||
administrator: true,
|
administrator: true,
|
||||||
roles: [],
|
roles: [],
|
||||||
@@ -102,6 +107,14 @@ describe('<CourseOutline />', () => {
|
|||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseOutlineIndexApiUrl(courseId))
|
.onGet(getCourseOutlineIndexApiUrl(courseId))
|
||||||
.reply(200, courseOutlineIndexMock);
|
.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);
|
await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,6 +127,13 @@ describe('<CourseOutline />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render permissionDenied if incorrect permissions', async () => {
|
||||||
|
const { getByTestId } = render(<RootWrapper />);
|
||||||
|
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||||
|
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||||
|
expect(getByTestId('permissionDeniedAlert')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
it('check reindex and render success alert is correctly', async () => {
|
it('check reindex and render success alert is correctly', async () => {
|
||||||
const { findByText, findByTestId } = render(<RootWrapper />);
|
const { findByText, findByTestId } = render(<RootWrapper />);
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,13 @@ import AddTeamMember from './add-team-member/AddTeamMember';
|
|||||||
import CourseTeamMember from './course-team-member/CourseTeamMember';
|
import CourseTeamMember from './course-team-member/CourseTeamMember';
|
||||||
import InfoModal from './info-modal/InfoModal';
|
import InfoModal from './info-modal/InfoModal';
|
||||||
import { useCourseTeam } from './hooks';
|
import { useCourseTeam } from './hooks';
|
||||||
|
import { useUserPermissions } from '../generic/hooks';
|
||||||
import getPageHeadTitle from '../generic/utils';
|
import getPageHeadTitle from '../generic/utils';
|
||||||
|
|
||||||
const CourseTeam = ({ courseId }) => {
|
const CourseTeam = ({ courseId }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const courseDetails = useModel('courseDetails', courseId);
|
const courseDetails = useModel('courseDetails', courseId);
|
||||||
|
|
||||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
|
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -55,6 +56,12 @@ const CourseTeam = ({ courseId }) => {
|
|||||||
handleInternetConnectionFailed,
|
handleInternetConnectionFailed,
|
||||||
} = useCourseTeam({ intl, courseId });
|
} = useCourseTeam({ intl, courseId });
|
||||||
|
|
||||||
|
const {
|
||||||
|
checkPermission,
|
||||||
|
} = useUserPermissions();
|
||||||
|
|
||||||
|
const hasManageAllUsersPerm = checkPermission('manage_all_users');
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
return <></>;
|
return <></>;
|
||||||
@@ -77,7 +84,7 @@ const CourseTeam = ({ courseId }) => {
|
|||||||
<SubHeader
|
<SubHeader
|
||||||
title={intl.formatMessage(messages.headingTitle)}
|
title={intl.formatMessage(messages.headingTitle)}
|
||||||
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
||||||
headerActions={isAllowActions && (
|
headerActions={(isAllowActions || hasManageAllUsersPerm) && (
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
iconBefore={IconAdd}
|
iconBefore={IconAdd}
|
||||||
@@ -104,13 +111,13 @@ const CourseTeam = ({ courseId }) => {
|
|||||||
role={role}
|
role={role}
|
||||||
email={email}
|
email={email}
|
||||||
currentUserEmail={currentUserEmail || ''}
|
currentUserEmail={currentUserEmail || ''}
|
||||||
isAllowActions={isAllowActions}
|
isAllowActions={isAllowActions || hasManageAllUsersPerm}
|
||||||
isHideActions={role === USER_ROLES.admin && isSingleAdmin}
|
isHideActions={role === USER_ROLES.admin && isSingleAdmin}
|
||||||
onChangeRole={handleChangeRoleUserSubmit}
|
onChangeRole={handleChangeRoleUserSubmit}
|
||||||
onDelete={handleOpenDeleteModal}
|
onDelete={handleOpenDeleteModal}
|
||||||
/>
|
/>
|
||||||
)) : null}
|
)) : null}
|
||||||
{isShowAddTeamMember && (
|
{(isShowAddTeamMember || hasManageAllUsersPerm) && (
|
||||||
<AddTeamMember
|
<AddTeamMember
|
||||||
onFormOpen={openForm}
|
onFormOpen={openForm}
|
||||||
isButtonDisable={isFormVisible}
|
isButtonDisable={isFormVisible}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
|||||||
import initializeStore from '../store';
|
import initializeStore from '../store';
|
||||||
import { courseTeamMock, courseTeamWithOneUser, courseTeamWithoutUsers } from './__mocks__';
|
import { courseTeamMock, courseTeamWithOneUser, courseTeamWithoutUsers } from './__mocks__';
|
||||||
import { getCourseTeamApiUrl, updateCourseTeamUserApiUrl } from './data/api';
|
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 CourseTeam from './CourseTeam';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { USER_ROLES } from '../constants';
|
import { USER_ROLES } from '../constants';
|
||||||
@@ -24,6 +26,8 @@ let axiosMock;
|
|||||||
let store;
|
let store;
|
||||||
const mockPathname = '/foo-bar';
|
const mockPathname = '/foo-bar';
|
||||||
const courseId = '123';
|
const courseId = '123';
|
||||||
|
const userId = 3;
|
||||||
|
const userPermissionsData = { permissions: ['manage_all_users'] };
|
||||||
|
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('react-router-dom'),
|
||||||
@@ -44,7 +48,7 @@ describe('<CourseTeam />', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
initializeMockApp({
|
initializeMockApp({
|
||||||
authenticatedUser: {
|
authenticatedUser: {
|
||||||
userId: 3,
|
userId,
|
||||||
username: 'abc123',
|
username: 'abc123',
|
||||||
administrator: true,
|
administrator: true,
|
||||||
roles: [],
|
roles: [],
|
||||||
@@ -53,6 +57,12 @@ describe('<CourseTeam />', () => {
|
|||||||
|
|
||||||
store = initializeStore();
|
store = initializeStore();
|
||||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
axiosMock
|
||||||
|
.onGet(getUserPermissionsEnabledFlagUrl)
|
||||||
|
.reply(200, { enabled: true });
|
||||||
|
axiosMock
|
||||||
|
.onGet(getUserPermissionsUrl(courseId, userId))
|
||||||
|
.reply(200, userPermissionsData);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('render CourseTeam component with 3 team members correctly', async () => {
|
it('render CourseTeam component with 3 team members correctly', async () => {
|
||||||
@@ -165,13 +175,13 @@ describe('<CourseTeam />', () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(queryByTestId('add-user-form')).not.toBeInTheDocument();
|
expect(queryByTestId('add-user-form')).not.toBeInTheDocument();
|
||||||
const addButton = getByRole('button', { name: 'Add a new team member' });
|
const addButton = getByRole('button', { name: messages.addNewMemberButton.defaultMessage });
|
||||||
fireEvent.click(addButton);
|
fireEvent.click(addButton);
|
||||||
expect(queryByTestId('add-user-form')).toBeInTheDocument();
|
expect(queryByTestId('add-user-form')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('not displays "Add New Member" and AddTeamMember component when isAllowActions is false', async () => {
|
it('not displays "Add New Member" and AddTeamMember component when isAllowActions is false and hasManageAllUsersPerm is false', async () => {
|
||||||
cleanup();
|
cleanup();
|
||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseTeamApiUrl(courseId))
|
.onGet(getCourseTeamApiUrl(courseId))
|
||||||
@@ -179,12 +189,39 @@ describe('<CourseTeam />', () => {
|
|||||||
...courseTeamWithOneUser,
|
...courseTeamWithOneUser,
|
||||||
allowActions: false,
|
allowActions: false,
|
||||||
});
|
});
|
||||||
|
axiosMock
|
||||||
|
.onGet(getUserPermissionsEnabledFlagUrl)
|
||||||
|
.reply(200, { enabled: true });
|
||||||
|
|
||||||
const { queryByRole, queryByTestId } = render(<RootWrapper />);
|
const { queryByRole, queryByText } = render(<RootWrapper />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(queryByRole('button', { name: messages.addNewMemberButton.defaultMessage })).not.toBeInTheDocument();
|
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(<RootWrapper />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByRole('button', { name: messages.addNewMemberButton.defaultMessage })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const CourseTeamMember = ({
|
|||||||
<div className="course-team-member" data-testid="course-team-member">
|
<div className="course-team-member" data-testid="course-team-member">
|
||||||
<div className="member-info">
|
<div className="member-info">
|
||||||
<Badge className={`badge-current-user bg-${badgeColor} text-light-100`}>
|
<Badge className={`badge-current-user bg-${badgeColor} text-light-100`}>
|
||||||
{isAdminRole
|
{(isAdminRole)
|
||||||
? intl.formatMessage(messages.roleAdmin)
|
? intl.formatMessage(messages.roleAdmin)
|
||||||
: intl.formatMessage(messages.roleStaff)}
|
: intl.formatMessage(messages.roleStaff)}
|
||||||
{currentUserEmail === email && (
|
{currentUserEmail === email && (
|
||||||
@@ -46,11 +46,13 @@ const CourseTeamMember = ({
|
|||||||
!isHideActions ? (
|
!isHideActions ? (
|
||||||
<div className="member-actions">
|
<div className="member-actions">
|
||||||
<Button
|
<Button
|
||||||
variant={isAdminRole ? 'tertiary' : 'primary'}
|
variant={(isAdminRole) ? 'tertiary' : 'primary'}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onChangeRole(email, isAdminRole ? USER_ROLES.staff : USER_ROLES.admin)}
|
onClick={() => onChangeRole(email, isAdminRole ? USER_ROLES.staff : USER_ROLES.admin)}
|
||||||
>
|
>
|
||||||
{isAdminRole ? intl.formatMessage(messages.removeButton) : intl.formatMessage(messages.addButton)}
|
{(isAdminRole)
|
||||||
|
? intl.formatMessage(messages.removeButton)
|
||||||
|
: intl.formatMessage(messages.addButton)}
|
||||||
</Button>
|
</Button>
|
||||||
<IconButtonWithTooltip
|
<IconButtonWithTooltip
|
||||||
src={DeleteOutline}
|
src={DeleteOutline}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import {
|
|||||||
} from '@edx/paragon';
|
} from '@edx/paragon';
|
||||||
import { Add as AddIcon } from '@edx/paragon/icons';
|
import { Add as AddIcon } from '@edx/paragon/icons';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { useModel } from '../generic/model-store';
|
import { useModel } from '../generic/model-store';
|
||||||
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
|
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
|
||||||
import ProcessingNotification from '../generic/processing-notification';
|
import ProcessingNotification from '../generic/processing-notification';
|
||||||
import SubHeader from '../generic/sub-header/SubHeader';
|
import SubHeader from '../generic/sub-header/SubHeader';
|
||||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||||
|
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
|
||||||
import { RequestStatus } from '../data/constants';
|
import { RequestStatus } from '../data/constants';
|
||||||
import CourseHandouts from './course-handouts/CourseHandouts';
|
import CourseHandouts from './course-handouts/CourseHandouts';
|
||||||
import CourseUpdate from './course-update/CourseUpdate';
|
import CourseUpdate from './course-update/CourseUpdate';
|
||||||
@@ -25,6 +25,8 @@ import { useCourseUpdates } from './hooks';
|
|||||||
import { getLoadingStatuses, getSavingStatuses } from './data/selectors';
|
import { getLoadingStatuses, getSavingStatuses } from './data/selectors';
|
||||||
import { matchesAnyStatus } from './utils';
|
import { matchesAnyStatus } from './utils';
|
||||||
import getPageHeadTitle from '../generic/utils';
|
import getPageHeadTitle from '../generic/utils';
|
||||||
|
import { getUserPermissionsEnabled } from '../generic/data/selectors';
|
||||||
|
import { useUserPermissions } from '../generic/hooks';
|
||||||
|
|
||||||
const CourseUpdates = ({ courseId }) => {
|
const CourseUpdates = ({ courseId }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -60,7 +62,15 @@ const CourseUpdates = ({ courseId }) => {
|
|||||||
const anyStatusFailed = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.FAILED);
|
const anyStatusFailed = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.FAILED);
|
||||||
const anyStatusInProgress = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.IN_PROGRESS);
|
const anyStatusInProgress = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.IN_PROGRESS);
|
||||||
const anyStatusPending = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.PENDING);
|
const anyStatusPending = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.PENDING);
|
||||||
|
const { checkPermission } = useUserPermissions();
|
||||||
|
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||||
|
const showPermissionDeniedAlert = userPermissionsEnabled && !checkPermission('manage_content');
|
||||||
|
|
||||||
|
if (showPermissionDeniedAlert) {
|
||||||
|
return (
|
||||||
|
<PermissionDeniedAlert />
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container size="xl" className="px-4">
|
<Container size="xl" className="px-4">
|
||||||
|
|||||||
@@ -22,11 +22,16 @@ import { executeThunk } from '../utils';
|
|||||||
import { courseUpdatesMock, courseHandoutsMock } from './__mocks__';
|
import { courseUpdatesMock, courseHandoutsMock } from './__mocks__';
|
||||||
import CourseUpdates from './CourseUpdates';
|
import CourseUpdates from './CourseUpdates';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api';
|
||||||
|
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
|
||||||
|
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
let store;
|
let store;
|
||||||
const mockPathname = '/foo-bar';
|
const mockPathname = '/foo-bar';
|
||||||
const courseId = '123';
|
const courseId = '123';
|
||||||
|
const userId = 3;
|
||||||
|
const userPermissionsData = { permissions: ['manage_content'] };
|
||||||
|
const wrongUserPermissionsData = { permissions: ['wrong_permission'] };
|
||||||
|
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('react-router-dom'),
|
||||||
@@ -61,7 +66,7 @@ const RootWrapper = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
describe('<CourseUpdates />', () => {
|
describe('<CourseUpdates />', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
initializeMockApp({
|
initializeMockApp({
|
||||||
authenticatedUser: {
|
authenticatedUser: {
|
||||||
userId: 3,
|
userId: 3,
|
||||||
@@ -79,6 +84,7 @@ describe('<CourseUpdates />', () => {
|
|||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getCourseHandoutApiUrl(courseId))
|
.onGet(getCourseHandoutApiUrl(courseId))
|
||||||
.reply(200, courseHandoutsMock);
|
.reply(200, courseHandoutsMock);
|
||||||
|
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('render CourseUpdates component correctly', async () => {
|
it('render CourseUpdates component correctly', async () => {
|
||||||
@@ -162,6 +168,26 @@ describe('<CourseUpdates />', () => {
|
|||||||
expect(queryByText(courseHandoutsMock.data)).not.toBeInTheDocument();
|
expect(queryByText(courseHandoutsMock.data)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should shows PermissionDeniedAlert if there are no right User Permissions', async () => {
|
||||||
|
const { getByTestId } = render(<RootWrapper />);
|
||||||
|
|
||||||
|
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(<RootWrapper />);
|
||||||
|
|
||||||
|
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 () => {
|
it('Add new update form is visible after clicking "New update" button', async () => {
|
||||||
const { getByText, getByRole, getAllByTestId } = render(<RootWrapper />);
|
const { getByText, getByRole, getAllByTestId } = render(<RootWrapper />);
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ import { updateExportTriggered, updateSavingStatus, updateSuccessDate } from './
|
|||||||
import ExportModalError from './export-modal-error/ExportModalError';
|
import ExportModalError from './export-modal-error/ExportModalError';
|
||||||
import ExportFooter from './export-footer/ExportFooter';
|
import ExportFooter from './export-footer/ExportFooter';
|
||||||
import ExportStepper from './export-stepper/ExportStepper';
|
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 CourseExportPage = ({ intl, courseId }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -38,6 +41,14 @@ const CourseExportPage = ({ intl, courseId }) => {
|
|||||||
const isShowExportButton = !exportTriggered || errorMessage || currentStage === EXPORT_STAGES.SUCCESS;
|
const isShowExportButton = !exportTriggered || errorMessage || currentStage === EXPORT_STAGES.SUCCESS;
|
||||||
const anyRequestFailed = savingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.FAILED;
|
const anyRequestFailed = savingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.FAILED;
|
||||||
const anyRequestInProgress = savingStatus === RequestStatus.PENDING || loadingStatus === RequestStatus.IN_PROGRESS;
|
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(() => {
|
useEffect(() => {
|
||||||
const cookieData = cookies.get(LAST_EXPORT_COOKIE_NAME);
|
const cookieData = cookies.get(LAST_EXPORT_COOKIE_NAME);
|
||||||
@@ -48,6 +59,12 @@ const CourseExportPage = ({ intl, courseId }) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
if (!hasExportPermissions) {
|
||||||
|
return (
|
||||||
|
<PermissionDeniedAlert />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@@ -89,13 +106,14 @@ const CourseExportPage = ({ intl, courseId }) => {
|
|||||||
className="mb-4"
|
className="mb-4"
|
||||||
onClick={() => dispatch(startExportingCourse(courseId))}
|
onClick={() => dispatch(startExportingCourse(courseId))}
|
||||||
iconBefore={ArrowCircleDownIcon}
|
iconBefore={ArrowCircleDownIcon}
|
||||||
|
disabled={viewOnly}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.buttonTitle)}
|
{intl.formatMessage(messages.buttonTitle)}
|
||||||
</Button>
|
</Button>
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
{exportTriggered && <ExportStepper courseId={courseId} />}
|
{exportTriggered && <ExportStepper courseId={courseId} viewOnly={viewOnly} />}
|
||||||
<ExportFooter />
|
<ExportFooter />
|
||||||
</article>
|
</article>
|
||||||
</Layout.Element>
|
</Layout.Element>
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import initializeStore from '../store';
|
|||||||
import stepperMessages from './export-stepper/messages';
|
import stepperMessages from './export-stepper/messages';
|
||||||
import modalErrorMessages from './export-modal-error/messages';
|
import modalErrorMessages from './export-modal-error/messages';
|
||||||
import { getExportStatusApiUrl, postExportCourseApiUrl } from './data/api';
|
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 { EXPORT_STAGES } from './data/constants';
|
||||||
import { exportPageMock } from './__mocks__';
|
import { exportPageMock } from './__mocks__';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
@@ -22,6 +25,8 @@ let axiosMock;
|
|||||||
let cookies;
|
let cookies;
|
||||||
const courseId = '123';
|
const courseId = '123';
|
||||||
const courseName = 'About Node JS';
|
const courseName = 'About Node JS';
|
||||||
|
const userId = 3;
|
||||||
|
let userPermissionsData = { permissions: [] };
|
||||||
|
|
||||||
jest.mock('../generic/model-store', () => ({
|
jest.mock('../generic/model-store', () => ({
|
||||||
useModel: jest.fn().mockReturnValue({
|
useModel: jest.fn().mockReturnValue({
|
||||||
@@ -49,7 +54,7 @@ describe('<CourseExportPage />', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
initializeMockApp({
|
initializeMockApp({
|
||||||
authenticatedUser: {
|
authenticatedUser: {
|
||||||
userId: 3,
|
userId,
|
||||||
username: 'abc123',
|
username: 'abc123',
|
||||||
administrator: true,
|
administrator: true,
|
||||||
roles: [],
|
roles: [],
|
||||||
@@ -60,6 +65,14 @@ describe('<CourseExportPage />', () => {
|
|||||||
axiosMock
|
axiosMock
|
||||||
.onGet(postExportCourseApiUrl(courseId))
|
.onGet(postExportCourseApiUrl(courseId))
|
||||||
.reply(200, exportPageMock);
|
.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 = new Cookies();
|
||||||
cookies.get.mockReturnValue(null);
|
cookies.get.mockReturnValue(null);
|
||||||
});
|
});
|
||||||
@@ -85,8 +98,57 @@ describe('<CourseExportPage />', () => {
|
|||||||
expect(getByText(messages.buttonTitle.defaultMessage)).toBeInTheDocument();
|
expect(getByText(messages.buttonTitle.defaultMessage)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('should render permissionDenied if incorrect permissions', async () => {
|
||||||
|
const { getByTestId } = render(<RootWrapper />);
|
||||||
|
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||||
|
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||||
|
expect(getByTestId('permissionDeniedAlert')).toBeVisible();
|
||||||
|
});
|
||||||
|
it('should render without errors if correct permissions', async () => {
|
||||||
|
const { getByText } = render(<RootWrapper />);
|
||||||
|
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(<RootWrapper />);
|
||||||
|
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 () => {
|
it('should start exporting on click', async () => {
|
||||||
const { getByText, container } = render(<RootWrapper />);
|
const { getByText, container } = render(<RootWrapper />);
|
||||||
|
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||||
|
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||||
const button = container.querySelector('.btn-primary');
|
const button = container.querySelector('.btn-primary');
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
expect(getByText(stepperMessages.stepperPreparingDescription.defaultMessage)).toBeInTheDocument();
|
expect(getByText(stepperMessages.stepperPreparingDescription.defaultMessage)).toBeInTheDocument();
|
||||||
@@ -96,6 +158,8 @@ describe('<CourseExportPage />', () => {
|
|||||||
.onGet(getExportStatusApiUrl(courseId))
|
.onGet(getExportStatusApiUrl(courseId))
|
||||||
.reply(200, { exportStatus: EXPORT_STAGES.EXPORTING, exportError: { rawErrorMsg: 'test error', editUnitUrl: 'http://test-url.test' } });
|
.reply(200, { exportStatus: EXPORT_STAGES.EXPORTING, exportError: { rawErrorMsg: 'test error', editUnitUrl: 'http://test-url.test' } });
|
||||||
const { getByText, queryByText, container } = render(<RootWrapper />);
|
const { getByText, queryByText, container } = render(<RootWrapper />);
|
||||||
|
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||||
|
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||||
const startExportButton = container.querySelector('.btn-primary');
|
const startExportButton = container.querySelector('.btn-primary');
|
||||||
fireEvent.click(startExportButton);
|
fireEvent.click(startExportButton);
|
||||||
// eslint-disable-next-line no-promise-executor-return
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
@@ -116,6 +180,8 @@ describe('<CourseExportPage />', () => {
|
|||||||
.onGet(getExportStatusApiUrl(courseId))
|
.onGet(getExportStatusApiUrl(courseId))
|
||||||
.reply(200, { exportStatus: EXPORT_STAGES.SUCCESS, exportOutput: '/test-download-path.test' });
|
.reply(200, { exportStatus: EXPORT_STAGES.SUCCESS, exportOutput: '/test-download-path.test' });
|
||||||
const { getByText, container } = render(<RootWrapper />);
|
const { getByText, container } = render(<RootWrapper />);
|
||||||
|
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||||
|
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||||
const startExportButton = container.querySelector('.btn-primary');
|
const startExportButton = container.querySelector('.btn-primary');
|
||||||
fireEvent.click(startExportButton);
|
fireEvent.click(startExportButton);
|
||||||
// eslint-disable-next-line no-promise-executor-return
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
@@ -129,6 +195,8 @@ describe('<CourseExportPage />', () => {
|
|||||||
.onGet(getExportStatusApiUrl(courseId))
|
.onGet(getExportStatusApiUrl(courseId))
|
||||||
.reply(200, { exportStatus: EXPORT_STAGES.SUCCESS, exportOutput: 'http://test-download-path.test' });
|
.reply(200, { exportStatus: EXPORT_STAGES.SUCCESS, exportOutput: 'http://test-download-path.test' });
|
||||||
const { getByText, container } = render(<RootWrapper />);
|
const { getByText, container } = render(<RootWrapper />);
|
||||||
|
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||||
|
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||||
const startExportButton = container.querySelector('.btn-primary');
|
const startExportButton = container.querySelector('.btn-primary');
|
||||||
fireEvent.click(startExportButton);
|
fireEvent.click(startExportButton);
|
||||||
// eslint-disable-next-line no-promise-executor-return
|
// eslint-disable-next-line no-promise-executor-return
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { EXPORT_STAGES } from '../data/constants';
|
|||||||
import { RequestStatus } from '../../data/constants';
|
import { RequestStatus } from '../../data/constants';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const ExportStepper = ({ intl, courseId }) => {
|
const ExportStepper = ({ intl, courseId, viewOnly }) => {
|
||||||
const currentStage = useSelector(getCurrentStage);
|
const currentStage = useSelector(getCurrentStage);
|
||||||
const downloadPath = useSelector(getDownloadPath);
|
const downloadPath = useSelector(getDownloadPath);
|
||||||
const successDate = useSelector(getSuccessDate);
|
const successDate = useSelector(getSuccessDate);
|
||||||
@@ -90,7 +90,7 @@ const ExportStepper = ({ intl, courseId }) => {
|
|||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
hasError={!!errorMessage}
|
hasError={!!errorMessage}
|
||||||
/>
|
/>
|
||||||
{downloadPath && currentStage === EXPORT_STAGES.SUCCESS && <Button className="ml-5.5 mt-n2.5" href={downloadPath} download>{intl.formatMessage(messages.downloadCourseButtonTitle)}</Button>}
|
{downloadPath && currentStage === EXPORT_STAGES.SUCCESS && <Button className="ml-5.5 mt-n2.5" href={downloadPath} download disabled={viewOnly}>{intl.formatMessage(messages.downloadCourseButtonTitle)}</Button>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -98,6 +98,7 @@ const ExportStepper = ({ intl, courseId }) => {
|
|||||||
ExportStepper.propTypes = {
|
ExportStepper.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
courseId: PropTypes.string.isRequired,
|
courseId: PropTypes.string.isRequired,
|
||||||
|
viewOnly: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(ExportStepper);
|
export default injectIntl(ExportStepper);
|
||||||
|
|||||||
@@ -1,20 +1,36 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from '@testing-library/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 { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import { initializeMockApp } from '@edx/frontend-platform';
|
import { initializeMockApp } from '@edx/frontend-platform';
|
||||||
import { AppProvider } from '@edx/frontend-platform/react';
|
import { AppProvider } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
import initializeStore from '../../store';
|
import initializeStore from '../../store';
|
||||||
|
import { executeThunk } from '../../utils';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import ExportStepper from './ExportStepper';
|
import ExportStepper from './ExportStepper';
|
||||||
|
import { exportStepperPageMock } from './__mocks__';
|
||||||
|
import { fetchExportStatus } from '../data/thunks';
|
||||||
|
import { getExportStatusApiUrl } from '../data/api';
|
||||||
|
|
||||||
const courseId = 'course-123';
|
const courseId = 'course-123';
|
||||||
|
let axiosMock;
|
||||||
let store;
|
let store;
|
||||||
|
|
||||||
const RootWrapper = () => (
|
const RootWrapper = () => (
|
||||||
<AppProvider store={store}>
|
<AppProvider store={store}>
|
||||||
<IntlProvider locale="en" messages={{}}>
|
<IntlProvider locale="en" messages={{}}>
|
||||||
<ExportStepper intl={{ formatMessage: jest.fn() }} courseId={courseId} />
|
<ExportStepper intl={{ formatMessage: jest.fn() }} courseId={courseId} viewOnly={false} />
|
||||||
|
</IntlProvider>
|
||||||
|
</AppProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ViewOnlyRootWrapper = () => (
|
||||||
|
<AppProvider store={store}>
|
||||||
|
<IntlProvider locale="en" messages={{}}>
|
||||||
|
<ExportStepper intl={{ formatMessage: jest.fn() }} courseId={courseId} viewOnly />
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
);
|
);
|
||||||
@@ -30,9 +46,22 @@ describe('<ExportStepper />', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
store = initializeStore();
|
store = initializeStore();
|
||||||
|
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
axiosMock
|
||||||
|
.onGet(getExportStatusApiUrl(courseId))
|
||||||
|
.reply(200, exportStepperPageMock);
|
||||||
|
executeThunk(fetchExportStatus(courseId), store.dispatch);
|
||||||
});
|
});
|
||||||
it('render stepper correctly', () => {
|
it('render stepper correctly', () => {
|
||||||
const { getByText } = render(<RootWrapper />);
|
const { getByText } = render(<RootWrapper />);
|
||||||
expect(getByText(messages.stepperHeaderTitle.defaultMessage)).toBeInTheDocument();
|
expect(getByText(messages.stepperHeaderTitle.defaultMessage)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
it('render stepper correctly if button is disabled', () => {
|
||||||
|
const { getByText } = render(<ViewOnlyRootWrapper />);
|
||||||
|
expect(getByText(messages.stepperHeaderTitle.defaultMessage)).toBeInTheDocument();
|
||||||
|
const buttonElement = getByText(messages.downloadCourseButtonTitle.defaultMessage, {
|
||||||
|
selector: '.disabled',
|
||||||
|
});
|
||||||
|
expect(buttonElement).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
exportStatus: 3,
|
||||||
|
exportOutput: '/test',
|
||||||
|
};
|
||||||
2
src/export-page/export-stepper/__mocks__/index.js
Normal file
2
src/export-page/export-stepper/__mocks__/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export { default as exportStepperPageMock } from './exportStepperPage';
|
||||||
@@ -30,6 +30,9 @@ import {
|
|||||||
import { getFileSizeToClosestByte } from '../../utils';
|
import { getFileSizeToClosestByte } from '../../utils';
|
||||||
import FileThumbnail from './FileThumbnail';
|
import FileThumbnail from './FileThumbnail';
|
||||||
import FileInfoModalSidebar from './FileInfoModalSidebar';
|
import FileInfoModalSidebar from './FileInfoModalSidebar';
|
||||||
|
import { useUserPermissions } from '../../generic/hooks';
|
||||||
|
import { getUserPermissionsEnabled } from '../../generic/data/selectors';
|
||||||
|
import PermissionDeniedAlert from '../../generic/PermissionDeniedAlert';
|
||||||
|
|
||||||
const FilesPage = ({
|
const FilesPage = ({
|
||||||
courseId,
|
courseId,
|
||||||
@@ -39,6 +42,9 @@ const FilesPage = ({
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const courseDetails = useModel('courseDetails', courseId);
|
const courseDetails = useModel('courseDetails', courseId);
|
||||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading));
|
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading));
|
||||||
|
const { checkPermission } = useUserPermissions();
|
||||||
|
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||||
|
const showPermissionDeniedAlert = userPermissionsEnabled && !checkPermission('manage_content');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchAssets(courseId));
|
dispatch(fetchAssets(courseId));
|
||||||
@@ -160,6 +166,11 @@ const FilesPage = ({
|
|||||||
{ ...accessColumn },
|
{ ...accessColumn },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (showPermissionDeniedAlert) {
|
||||||
|
return (
|
||||||
|
<PermissionDeniedAlert />
|
||||||
|
);
|
||||||
|
}
|
||||||
if (loadingStatus === RequestStatus.DENIED) {
|
if (loadingStatus === RequestStatus.DENIED) {
|
||||||
return (
|
return (
|
||||||
<div data-testid="under-construction-placeholder" className="row justify-contnt-center m-6">
|
<div data-testid="under-construction-placeholder" className="row justify-contnt-center m-6">
|
||||||
|
|||||||
@@ -39,10 +39,16 @@ import {
|
|||||||
} from './data/thunks';
|
} from './data/thunks';
|
||||||
import { getAssetsUrl } from './data/api';
|
import { getAssetsUrl } from './data/api';
|
||||||
import messages from '../generic/messages';
|
import messages from '../generic/messages';
|
||||||
|
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../../generic/data/api';
|
||||||
|
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../../generic/data/thunks';
|
||||||
|
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
let store;
|
let store;
|
||||||
let file;
|
let file;
|
||||||
|
const userId = 3;
|
||||||
|
const wrongUserPermissionsData = { permissions: ['wrong_permission'] };
|
||||||
|
const userPermissionsData = { permissions: ['manage_content'] };
|
||||||
|
|
||||||
ReactDOM.createPortal = jest.fn(node => node);
|
ReactDOM.createPortal = jest.fn(node => node);
|
||||||
jest.mock('file-saver');
|
jest.mock('file-saver');
|
||||||
|
|
||||||
@@ -68,6 +74,8 @@ const mockStore = async (
|
|||||||
}
|
}
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await executeThunk(fetchAssets(courseId), store.dispatch);
|
await executeThunk(fetchAssets(courseId), store.dispatch);
|
||||||
|
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||||
|
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyMockStore = async (status) => {
|
const emptyMockStore = async (status) => {
|
||||||
@@ -75,6 +83,27 @@ const emptyMockStore = async (status) => {
|
|||||||
axiosMock.onGet(fetchAssetsUrl).reply(getStatusValue(status), generateEmptyApiResponse());
|
axiosMock.onGet(fetchAssetsUrl).reply(getStatusValue(status), generateEmptyApiResponse());
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await executeThunk(fetchAssets(courseId), store.dispatch);
|
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', () => {
|
describe('FilesAndUploads', () => {
|
||||||
@@ -82,7 +111,7 @@ describe('FilesAndUploads', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
initializeMockApp({
|
initializeMockApp({
|
||||||
authenticatedUser: {
|
authenticatedUser: {
|
||||||
userId: 3,
|
userId,
|
||||||
username: 'abc123',
|
username: 'abc123',
|
||||||
administrator: false,
|
administrator: false,
|
||||||
roles: [],
|
roles: [],
|
||||||
@@ -100,6 +129,21 @@ describe('FilesAndUploads', () => {
|
|||||||
file = new File(['(⌐□_□)'], 'download.png', { type: 'image/png' });
|
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 () => {
|
it('should return placeholder component', async () => {
|
||||||
await mockStore(RequestStatus.DENIED);
|
await mockStore(RequestStatus.DENIED);
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ import VideoThumbnail from './VideoThumbnail';
|
|||||||
import { getFormattedDuration, resampleFile } from './data/utils';
|
import { getFormattedDuration, resampleFile } from './data/utils';
|
||||||
import FILES_AND_UPLOAD_TYPE_FILTERS from '../generic/constants';
|
import FILES_AND_UPLOAD_TYPE_FILTERS from '../generic/constants';
|
||||||
import VideoInfoModalSidebar from './info-sidebar';
|
import VideoInfoModalSidebar from './info-sidebar';
|
||||||
|
import { useUserPermissions } from '../../generic/hooks';
|
||||||
|
import { getUserPermissionsEnabled } from '../../generic/data/selectors';
|
||||||
|
import PermissionDeniedAlert from '../../generic/PermissionDeniedAlert';
|
||||||
|
|
||||||
const VideosPage = ({
|
const VideosPage = ({
|
||||||
courseId,
|
courseId,
|
||||||
@@ -53,6 +56,9 @@ const VideosPage = ({
|
|||||||
const [isTranscriptSettingsOpen, openTranscriptSettings, closeTranscriptSettings] = useToggle(false);
|
const [isTranscriptSettingsOpen, openTranscriptSettings, closeTranscriptSettings] = useToggle(false);
|
||||||
const courseDetails = useModel('courseDetails', courseId);
|
const courseDetails = useModel('courseDetails', courseId);
|
||||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading));
|
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading));
|
||||||
|
const { checkPermission } = useUserPermissions();
|
||||||
|
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||||
|
const showPermissionDeniedAlert = userPermissionsEnabled && !checkPermission('manage_content');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchVideos(courseId));
|
dispatch(fetchVideos(courseId));
|
||||||
@@ -179,6 +185,11 @@ const VideosPage = ({
|
|||||||
{ ...processingStatusColumn },
|
{ ...processingStatusColumn },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (showPermissionDeniedAlert) {
|
||||||
|
return (
|
||||||
|
<PermissionDeniedAlert />
|
||||||
|
);
|
||||||
|
}
|
||||||
if (loadingStatus === RequestStatus.DENIED) {
|
if (loadingStatus === RequestStatus.DENIED) {
|
||||||
return (
|
return (
|
||||||
<div data-testid="under-construction-placeholder" className="row justify-contnt-center m-6">
|
<div data-testid="under-construction-placeholder" className="row justify-contnt-center m-6">
|
||||||
|
|||||||
@@ -39,10 +39,15 @@ import {
|
|||||||
import { getVideosUrl, getCourseVideosApiUrl, getApiBaseUrl } from './data/api';
|
import { getVideosUrl, getCourseVideosApiUrl, getApiBaseUrl } from './data/api';
|
||||||
import videoMessages from './messages';
|
import videoMessages from './messages';
|
||||||
import messages from '../generic/messages';
|
import messages from '../generic/messages';
|
||||||
|
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../../generic/data/api';
|
||||||
|
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../../generic/data/thunks';
|
||||||
|
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
let store;
|
let store;
|
||||||
let file;
|
let file;
|
||||||
|
const userId = 3;
|
||||||
|
const wrongUserPermissionsData = { permissions: ['wrong_permission'] };
|
||||||
|
const userPermissionsData = { permissions: ['manage_content'] };
|
||||||
jest.mock('file-saver');
|
jest.mock('file-saver');
|
||||||
|
|
||||||
const renderComponent = () => {
|
const renderComponent = () => {
|
||||||
@@ -55,9 +60,7 @@ const renderComponent = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockStore = async (
|
const mockStore = async (status) => {
|
||||||
status,
|
|
||||||
) => {
|
|
||||||
const fetchVideosUrl = getVideosUrl(courseId);
|
const fetchVideosUrl = getVideosUrl(courseId);
|
||||||
const videosData = generateFetchVideosApiResponse();
|
const videosData = generateFetchVideosApiResponse();
|
||||||
axiosMock.onGet(fetchVideosUrl).reply(getStatusValue(status), videosData);
|
axiosMock.onGet(fetchVideosUrl).reply(getStatusValue(status), videosData);
|
||||||
@@ -68,6 +71,8 @@ const mockStore = async (
|
|||||||
|
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await executeThunk(fetchVideos(courseId), store.dispatch);
|
await executeThunk(fetchVideos(courseId), store.dispatch);
|
||||||
|
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||||
|
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyMockStore = async (status) => {
|
const emptyMockStore = async (status) => {
|
||||||
@@ -75,6 +80,27 @@ const emptyMockStore = async (status) => {
|
|||||||
axiosMock.onGet(fetchVideosUrl).reply(getStatusValue(status), generateEmptyApiResponse());
|
axiosMock.onGet(fetchVideosUrl).reply(getStatusValue(status), generateEmptyApiResponse());
|
||||||
renderComponent();
|
renderComponent();
|
||||||
await executeThunk(fetchVideos(courseId), store.dispatch);
|
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', () => {
|
describe('Videos page', () => {
|
||||||
@@ -82,7 +108,7 @@ describe('Videos page', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
initializeMockApp({
|
initializeMockApp({
|
||||||
authenticatedUser: {
|
authenticatedUser: {
|
||||||
userId: 3,
|
userId,
|
||||||
username: 'abc123',
|
username: 'abc123',
|
||||||
administrator: false,
|
administrator: false,
|
||||||
roles: [],
|
roles: [],
|
||||||
@@ -146,13 +172,31 @@ describe('Videos page', () => {
|
|||||||
|
|
||||||
expect(screen.getByTestId('files-data-table')).toBeVisible();
|
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', () => {
|
describe('valid videos', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
initializeMockApp({
|
initializeMockApp({
|
||||||
authenticatedUser: {
|
authenticatedUser: {
|
||||||
userId: 3,
|
userId,
|
||||||
username: 'abc123',
|
username: 'abc123',
|
||||||
administrator: false,
|
administrator: false,
|
||||||
roles: [],
|
roles: [],
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export const initialState = {
|
|||||||
},
|
},
|
||||||
organizations: ['krisEdx', 'krisEd', 'DeveloperInc', 'importMit', 'testX', 'edX', 'developerInb'],
|
organizations: ['krisEdx', 'krisEd', 'DeveloperInc', 'importMit', 'testX', 'edX', 'developerInb'],
|
||||||
savingStatus: '',
|
savingStatus: '',
|
||||||
|
userPermissions: [],
|
||||||
|
userPermissionsEnabled: false,
|
||||||
},
|
},
|
||||||
studioHome: {
|
studioHome: {
|
||||||
loadingStatuses: {
|
loadingStatuses: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
import { convertObjectToSnakeCase } from '../../utils';
|
import { convertObjectToSnakeCase } from '../../utils';
|
||||||
|
|
||||||
@@ -8,7 +8,8 @@ export const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
|||||||
export const getCreateOrRerunCourseUrl = () => new URL('course/', getApiBaseUrl()).href;
|
export const getCreateOrRerunCourseUrl = () => new URL('course/', getApiBaseUrl()).href;
|
||||||
export const getCourseRerunUrl = (courseId) => new URL(`/api/contentstore/v1/course_rerun/${courseId}`, getApiBaseUrl()).href;
|
export const getCourseRerunUrl = (courseId) => new URL(`/api/contentstore/v1/course_rerun/${courseId}`, getApiBaseUrl()).href;
|
||||||
export const getOrganizationsUrl = () => new URL('organizations', getApiBaseUrl()).href;
|
export const getOrganizationsUrl = () => new URL('organizations', getApiBaseUrl()).href;
|
||||||
|
export const getUserPermissionsUrl = (courseId, userId) => `${getApiBaseUrl()}/api/course_roles/v1/user_permissions/?course_id=${encodeURIComponent(courseId)}&user_id=${userId}`;
|
||||||
|
export const getUserPermissionsEnabledFlagUrl = new URL('/api/course_roles/v1/user_permissions/enabled/', getApiBaseUrl()).href;
|
||||||
/**
|
/**
|
||||||
* Get's organizations data. Returns list of organization names.
|
* Get's organizations data. Returns list of organization names.
|
||||||
* @returns {Promise<string[]>}
|
* @returns {Promise<string[]>}
|
||||||
@@ -43,3 +44,22 @@ export async function createOrRerunCourse(courseData) {
|
|||||||
);
|
);
|
||||||
return camelCaseObject(data);
|
return camelCaseObject(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user course roles permissions.
|
||||||
|
* @param {string} courseId
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
*/
|
||||||
|
export async function getUserPermissions(courseId) {
|
||||||
|
const { userId } = getAuthenticatedUser();
|
||||||
|
|
||||||
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
|
.get(getUserPermissionsUrl(courseId, userId));
|
||||||
|
return camelCaseObject(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserPermissionsEnabledFlag() {
|
||||||
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
|
.get(getUserPermissionsEnabledFlagUrl);
|
||||||
|
return data || false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,9 +9,14 @@ import {
|
|||||||
getCreateOrRerunCourseUrl,
|
getCreateOrRerunCourseUrl,
|
||||||
getCourseRerunUrl,
|
getCourseRerunUrl,
|
||||||
getCourseRerun,
|
getCourseRerun,
|
||||||
|
getUserPermissions,
|
||||||
|
getUserPermissionsUrl,
|
||||||
|
getUserPermissionsEnabledFlag,
|
||||||
|
getUserPermissionsEnabledFlagUrl,
|
||||||
} from './api';
|
} from './api';
|
||||||
|
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
|
const courseId = 'course-123';
|
||||||
|
|
||||||
describe('generic api calls', () => {
|
describe('generic api calls', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -41,7 +46,6 @@ describe('generic api calls', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should get course rerun', async () => {
|
it('should get course rerun', async () => {
|
||||||
const courseId = 'course-mock-id';
|
|
||||||
const courseRerunData = {
|
const courseRerunData = {
|
||||||
allowUnicodeCourseId: false,
|
allowUnicodeCourseId: false,
|
||||||
courseCreatorStatus: 'granted',
|
courseCreatorStatus: 'granted',
|
||||||
@@ -72,4 +76,24 @@ describe('generic api calls', () => {
|
|||||||
expect(axiosMock.history.post[0].url).toEqual(getCreateOrRerunCourseUrl());
|
expect(axiosMock.history.post[0].url).toEqual(getCreateOrRerunCourseUrl());
|
||||||
expect(result).toEqual(courseRerunData);
|
expect(result).toEqual(courseRerunData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should get user permissions', async () => {
|
||||||
|
const permissionsData = { permissions: ['manage_all_users'] };
|
||||||
|
const queryUrl = getUserPermissionsUrl(courseId, 3);
|
||||||
|
axiosMock.onGet(queryUrl).reply(200, permissionsData);
|
||||||
|
const result = await getUserPermissions(courseId);
|
||||||
|
|
||||||
|
expect(axiosMock.history.get[0].url).toEqual(queryUrl);
|
||||||
|
expect(result).toEqual(permissionsData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get user permissions enabled flag', async () => {
|
||||||
|
const permissionsEnabledData = { enabled: true };
|
||||||
|
const queryUrl = getUserPermissionsEnabledFlagUrl;
|
||||||
|
axiosMock.onGet(queryUrl).reply(200, permissionsEnabledData);
|
||||||
|
const result = await getUserPermissionsEnabledFlag();
|
||||||
|
|
||||||
|
expect(axiosMock.history.get[0].url).toEqual(queryUrl);
|
||||||
|
expect(result).toEqual(permissionsEnabledData);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,3 +5,5 @@ export const getCourseData = (state) => state.generic.createOrRerunCourse.course
|
|||||||
export const getCourseRerunData = (state) => state.generic.createOrRerunCourse.courseRerunData;
|
export const getCourseRerunData = (state) => state.generic.createOrRerunCourse.courseRerunData;
|
||||||
export const getRedirectUrlObj = (state) => state.generic.createOrRerunCourse.redirectUrlObj;
|
export const getRedirectUrlObj = (state) => state.generic.createOrRerunCourse.redirectUrlObj;
|
||||||
export const getPostErrors = (state) => state.generic.createOrRerunCourse.postErrors;
|
export const getPostErrors = (state) => state.generic.createOrRerunCourse.postErrors;
|
||||||
|
export const getUserPermissions = (state) => state.generic.userPermissions.permissions;
|
||||||
|
export const getUserPermissionsEnabled = (state) => state.generic.userPermissionsEnabled;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ const slice = createSlice({
|
|||||||
loadingStatuses: {
|
loadingStatuses: {
|
||||||
organizationLoadingStatus: RequestStatus.IN_PROGRESS,
|
organizationLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||||
courseRerunLoadingStatus: RequestStatus.IN_PROGRESS,
|
courseRerunLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||||
|
userPermissionsLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||||
|
userPermissionsEnabledLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||||
},
|
},
|
||||||
savingStatus: '',
|
savingStatus: '',
|
||||||
organizations: [],
|
organizations: [],
|
||||||
@@ -18,6 +20,8 @@ const slice = createSlice({
|
|||||||
redirectUrlObj: {},
|
redirectUrlObj: {},
|
||||||
postErrors: {},
|
postErrors: {},
|
||||||
},
|
},
|
||||||
|
userPermissions: [],
|
||||||
|
userPermissionsEnabled: false,
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
fetchOrganizations: (state, { payload }) => {
|
fetchOrganizations: (state, { payload }) => {
|
||||||
@@ -41,6 +45,12 @@ const slice = createSlice({
|
|||||||
updatePostErrors: (state, { payload }) => {
|
updatePostErrors: (state, { payload }) => {
|
||||||
state.createOrRerunCourse.postErrors = payload;
|
state.createOrRerunCourse.postErrors = payload;
|
||||||
},
|
},
|
||||||
|
updateUserPermissions: (state, { payload }) => {
|
||||||
|
state.userPermissions = payload;
|
||||||
|
},
|
||||||
|
updateUserPermissionsEnabled: (state, { payload }) => {
|
||||||
|
state.userPermissionsEnabled = payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,6 +62,8 @@ export const {
|
|||||||
updateSavingStatus,
|
updateSavingStatus,
|
||||||
updateCourseData,
|
updateCourseData,
|
||||||
updateRedirectUrlObj,
|
updateRedirectUrlObj,
|
||||||
|
updateUserPermissions,
|
||||||
|
updateUserPermissionsEnabled,
|
||||||
} = slice.actions;
|
} = slice.actions;
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { RequestStatus } from '../../data/constants';
|
import { RequestStatus } from '../../data/constants';
|
||||||
import { createOrRerunCourse, getOrganizations, getCourseRerun } from './api';
|
import {
|
||||||
|
createOrRerunCourse, getOrganizations, getCourseRerun, getUserPermissions, getUserPermissionsEnabledFlag,
|
||||||
|
} from './api';
|
||||||
import {
|
import {
|
||||||
fetchOrganizations,
|
fetchOrganizations,
|
||||||
updatePostErrors,
|
updatePostErrors,
|
||||||
@@ -7,6 +9,8 @@ import {
|
|||||||
updateRedirectUrlObj,
|
updateRedirectUrlObj,
|
||||||
updateCourseRerunData,
|
updateCourseRerunData,
|
||||||
updateSavingStatus,
|
updateSavingStatus,
|
||||||
|
updateUserPermissions,
|
||||||
|
updateUserPermissionsEnabled,
|
||||||
} from './slice';
|
} from './slice';
|
||||||
|
|
||||||
export function fetchOrganizationsQuery() {
|
export function fetchOrganizationsQuery() {
|
||||||
@@ -49,3 +53,28 @@ export function updateCreateOrRerunCourseQuery(courseData) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchUserPermissionsQuery(courseId) {
|
||||||
|
return async (dispatch) => {
|
||||||
|
try {
|
||||||
|
const userPermissions = await getUserPermissions(courseId);
|
||||||
|
dispatch(updateUserPermissions(userPermissions));
|
||||||
|
dispatch(updateLoadingStatuses({ userPermissionsLoadingStatus: RequestStatus.SUCCESSFUL }));
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(updateLoadingStatuses({ userPermissionsLoadingStatus: RequestStatus.FAILED }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchUserPermissionsEnabledFlag() {
|
||||||
|
return async (dispatch) => {
|
||||||
|
try {
|
||||||
|
const data = await getUserPermissionsEnabledFlag();
|
||||||
|
dispatch(updateUserPermissionsEnabled(data.enabled || false));
|
||||||
|
dispatch(updateLoadingStatuses({ userPermissionsEnabledLoadingStatus: RequestStatus.SUCCESSFUL }));
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(updateUserPermissionsEnabled(false));
|
||||||
|
dispatch(updateLoadingStatuses({ userPermissionsEnabledLoadingStatus: RequestStatus.FAILED }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
21
src/generic/hooks.jsx
Normal file
21
src/generic/hooks.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { getUserPermissions, getUserPermissionsEnabled } from './data/selectors';
|
||||||
|
|
||||||
|
const useUserPermissions = () => {
|
||||||
|
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||||
|
const userPermissions = useSelector(getUserPermissions);
|
||||||
|
|
||||||
|
const checkPermission = (permission) => {
|
||||||
|
if (!userPermissionsEnabled || !Array.isArray(userPermissions)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return userPermissions.includes(permission);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
checkPermission,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export { useUserPermissions };
|
||||||
@@ -30,6 +30,9 @@ import CreditSection from './credit-section';
|
|||||||
import DeadlineSection from './deadline-section';
|
import DeadlineSection from './deadline-section';
|
||||||
import { useConvertGradeCutoffs, useUpdateGradingData } from './hooks';
|
import { useConvertGradeCutoffs, useUpdateGradingData } from './hooks';
|
||||||
import getPageHeadTitle from '../generic/utils';
|
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 GradingSettings = ({ intl, courseId }) => {
|
||||||
const gradingSettingsData = useSelector(getGradingSettings);
|
const gradingSettingsData = useSelector(getGradingSettings);
|
||||||
@@ -43,6 +46,14 @@ const GradingSettings = ({ intl, courseId }) => {
|
|||||||
const [isQueryPending, setIsQueryPending] = useState(false);
|
const [isQueryPending, setIsQueryPending] = useState(false);
|
||||||
const [showOverrideInternetConnectionAlert, setOverrideInternetConnectionAlert] = useState(false);
|
const [showOverrideInternetConnectionAlert, setOverrideInternetConnectionAlert] = useState(false);
|
||||||
const [eligibleGrade, setEligibleGrade] = useState(null);
|
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);
|
const courseDetails = useModel('courseDetails', courseId);
|
||||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
|
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
|
||||||
@@ -83,6 +94,12 @@ const GradingSettings = ({ intl, courseId }) => {
|
|||||||
dispatch(fetchCourseSettingsQuery(courseId));
|
dispatch(fetchCourseSettingsQuery(courseId));
|
||||||
}, [courseId]);
|
}, [courseId]);
|
||||||
|
|
||||||
|
if (!hasGradingPermissions) {
|
||||||
|
return (
|
||||||
|
<PermissionDeniedAlert />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
return <></>;
|
return <></>;
|
||||||
@@ -156,6 +173,7 @@ const GradingSettings = ({ intl, courseId }) => {
|
|||||||
resetDataRef={resetDataRef}
|
resetDataRef={resetDataRef}
|
||||||
setOverrideInternetConnectionAlert={setOverrideInternetConnectionAlert}
|
setOverrideInternetConnectionAlert={setOverrideInternetConnectionAlert}
|
||||||
setEligibleGrade={setEligibleGrade}
|
setEligibleGrade={setEligibleGrade}
|
||||||
|
viewOnly={viewOnly}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
{courseSettingsData.creditEligibilityEnabled && courseSettingsData.isCreditCourse && (
|
{courseSettingsData.creditEligibilityEnabled && courseSettingsData.isCreditCourse && (
|
||||||
@@ -170,6 +188,7 @@ const GradingSettings = ({ intl, courseId }) => {
|
|||||||
minimumGradeCredit={minimumGradeCredit}
|
minimumGradeCredit={minimumGradeCredit}
|
||||||
setGradingData={setGradingData}
|
setGradingData={setGradingData}
|
||||||
setShowSuccessAlert={setShowSuccessAlert}
|
setShowSuccessAlert={setShowSuccessAlert}
|
||||||
|
viewOnly={viewOnly}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
@@ -183,6 +202,7 @@ const GradingSettings = ({ intl, courseId }) => {
|
|||||||
gracePeriod={gracePeriod}
|
gracePeriod={gracePeriod}
|
||||||
setGradingData={setGradingData}
|
setGradingData={setGradingData}
|
||||||
setShowSuccessAlert={setShowSuccessAlert}
|
setShowSuccessAlert={setShowSuccessAlert}
|
||||||
|
viewOnly={viewOnly}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
@@ -201,11 +221,13 @@ const GradingSettings = ({ intl, courseId }) => {
|
|||||||
setGradingData={setGradingData}
|
setGradingData={setGradingData}
|
||||||
courseAssignmentLists={courseAssignmentLists}
|
courseAssignmentLists={courseAssignmentLists}
|
||||||
setShowSuccessAlert={setShowSuccessAlert}
|
setShowSuccessAlert={setShowSuccessAlert}
|
||||||
|
viewOnly={viewOnly}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
iconBefore={IconAdd}
|
iconBefore={IconAdd}
|
||||||
onClick={handleAddAssignment}
|
onClick={handleAddAssignment}
|
||||||
|
disabled={viewOnly}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.addNewAssignmentTypeBtn)}
|
{intl.formatMessage(messages.addNewAssignmentTypeBtn)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -12,9 +12,15 @@ import gradingSettings from './__mocks__/gradingSettings';
|
|||||||
import GradingSettings from './GradingSettings';
|
import GradingSettings from './GradingSettings';
|
||||||
import messages from './messages';
|
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 courseId = '123';
|
||||||
|
const userId = 3;
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
let store;
|
let store;
|
||||||
|
const userPermissionsData = { permissions: [] };
|
||||||
|
|
||||||
const RootWrapper = () => (
|
const RootWrapper = () => (
|
||||||
<AppProvider store={store}>
|
<AppProvider store={store}>
|
||||||
@@ -28,7 +34,7 @@ describe('<GradingSettings />', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
initializeMockApp({
|
initializeMockApp({
|
||||||
authenticatedUser: {
|
authenticatedUser: {
|
||||||
userId: 3,
|
userId,
|
||||||
username: 'abc123',
|
username: 'abc123',
|
||||||
administrator: true,
|
administrator: true,
|
||||||
roles: [],
|
roles: [],
|
||||||
@@ -40,6 +46,25 @@ describe('<GradingSettings />', () => {
|
|||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getGradingSettingsApiUrl(courseId))
|
.onGet(getGradingSettingsApiUrl(courseId))
|
||||||
.reply(200, gradingSettings);
|
.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 () => {
|
it('should render without errors', async () => {
|
||||||
@@ -54,6 +79,38 @@ describe('<GradingSettings />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render without errors if access controlled by permissions', async () => {
|
||||||
|
const { getByText, getAllByText, getAllByTestId } = render(<RootWrapper />);
|
||||||
|
axiosMock
|
||||||
|
.onGet(getUserPermissionsEnabledFlagUrl)
|
||||||
|
.reply(200, { enabled: true });
|
||||||
|
const permissionsData = { permissions: ['manage_course_settings'] };
|
||||||
|
axiosMock
|
||||||
|
.onGet(getUserPermissionsUrl(courseId, userId))
|
||||||
|
.reply(200, permissionsData);
|
||||||
|
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||||
|
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||||
|
await waitFor(() => {
|
||||||
|
const gradingElements = getAllByText(messages.headingTitle.defaultMessage);
|
||||||
|
const gradingTitle = gradingElements[0];
|
||||||
|
const segmentButton = getAllByTestId('grading-scale-btn-add-segment');
|
||||||
|
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(gradingTitle).toBeInTheDocument();
|
||||||
|
expect(getByText(messages.policy.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(getByText(messages.policiesDescription.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(segmentButton.length).toEqual(1);
|
||||||
|
expect(segmentButton[0]).toBeInTheDocument();
|
||||||
|
expect(segmentButton[0].disabled).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render permissionDenied if incorrect permissions', async () => {
|
||||||
|
const { getByTestId } = render(<RootWrapper />);
|
||||||
|
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||||
|
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||||
|
expect(getByTestId('permissionDeniedAlert')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
it('should update segment input value and show save alert', async () => {
|
it('should update segment input value and show save alert', async () => {
|
||||||
const { getByTestId, getAllByTestId } = render(<RootWrapper />);
|
const { getByTestId, getAllByTestId } = render(<RootWrapper />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -76,6 +133,7 @@ describe('<GradingSettings />', () => {
|
|||||||
expect(segmentInput).toHaveValue('a');
|
expect(segmentInput).toHaveValue('a');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should save segment input changes and display saving message', async () => {
|
it('should save segment input changes and display saving message', async () => {
|
||||||
const { getByText, getAllByTestId } = render(<RootWrapper />);
|
const { getByText, getAllByTestId } = render(<RootWrapper />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -88,4 +146,23 @@ describe('<GradingSettings />', () => {
|
|||||||
expect(getByText(messages.buttonSavingText.defaultMessage)).toBeInTheDocument();
|
expect(getByText(messages.buttonSavingText.defaultMessage)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show disabled button to update segments', async () => {
|
||||||
|
const { getAllByTestId } = render(<RootWrapper />);
|
||||||
|
axiosMock
|
||||||
|
.onGet(getUserPermissionsEnabledFlagUrl)
|
||||||
|
.reply(200, { enabled: true });
|
||||||
|
const permissionsData = { permissions: ['view_course_settings'] };
|
||||||
|
axiosMock
|
||||||
|
.onGet(getUserPermissionsUrl(courseId, userId))
|
||||||
|
.reply(200, permissionsData);
|
||||||
|
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||||
|
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||||
|
await waitFor(() => {
|
||||||
|
const segmentButton = getAllByTestId('grading-scale-btn-add-segment');
|
||||||
|
expect(segmentButton.length).toEqual(1);
|
||||||
|
expect(segmentButton[0]).toBeInTheDocument();
|
||||||
|
expect(segmentButton[0].disabled).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const AssignmentItem = ({
|
|||||||
secondErrorMsg,
|
secondErrorMsg,
|
||||||
gradeField,
|
gradeField,
|
||||||
trailingElement,
|
trailingElement,
|
||||||
|
viewOnly,
|
||||||
}) => (
|
}) => (
|
||||||
<li className={className}>
|
<li className={className}>
|
||||||
<Form.Group className={classNames('form-group-custom', {
|
<Form.Group className={classNames('form-group-custom', {
|
||||||
@@ -37,6 +38,7 @@ const AssignmentItem = ({
|
|||||||
value={value}
|
value={value}
|
||||||
isInvalid={errorEffort}
|
isInvalid={errorEffort}
|
||||||
trailingElement={trailingElement}
|
trailingElement={trailingElement}
|
||||||
|
disabled={viewOnly}
|
||||||
/>
|
/>
|
||||||
<Form.Control.Feedback className="grading-description">
|
<Form.Control.Feedback className="grading-description">
|
||||||
{descriptions}
|
{descriptions}
|
||||||
@@ -81,6 +83,7 @@ AssignmentItem.propTypes = {
|
|||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
gradeField: PropTypes.shape(defaultAssignmentsPropTypes),
|
gradeField: PropTypes.shape(defaultAssignmentsPropTypes),
|
||||||
trailingElement: PropTypes.string,
|
trailingElement: PropTypes.string,
|
||||||
|
viewOnly: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AssignmentItem;
|
export default AssignmentItem;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { ASSIGNMENT_TYPES, DUPLICATE_ASSIGNMENT_NAME } from '../utils/enum';
|
|||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
const AssignmentTypeName = ({
|
const AssignmentTypeName = ({
|
||||||
intl, value, errorEffort, onChange,
|
intl, value, errorEffort, onChange, viewOnly,
|
||||||
}) => {
|
}) => {
|
||||||
const initialAssignmentName = useRef(value);
|
const initialAssignmentName = useRef(value);
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ const AssignmentTypeName = ({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={value}
|
value={value}
|
||||||
isInvalid={Boolean(errorEffort)}
|
isInvalid={Boolean(errorEffort)}
|
||||||
|
disabled={viewOnly}
|
||||||
/>
|
/>
|
||||||
<Form.Control.Feedback className="grading-description">
|
<Form.Control.Feedback className="grading-description">
|
||||||
{intl.formatMessage(messages.assignmentTypeNameDescription)}
|
{intl.formatMessage(messages.assignmentTypeNameDescription)}
|
||||||
@@ -64,6 +65,7 @@ AssignmentTypeName.propTypes = {
|
|||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
errorEffort: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
errorEffort: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired,
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired,
|
||||||
|
viewOnly: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(AssignmentTypeName);
|
export default injectIntl(AssignmentTypeName);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const AssignmentSection = ({
|
|||||||
setGradingData,
|
setGradingData,
|
||||||
courseAssignmentLists,
|
courseAssignmentLists,
|
||||||
setShowSuccessAlert,
|
setShowSuccessAlert,
|
||||||
|
viewOnly,
|
||||||
}) => {
|
}) => {
|
||||||
const [errorList, setErrorList] = useState({});
|
const [errorList, setErrorList] = useState({});
|
||||||
const {
|
const {
|
||||||
@@ -83,6 +84,7 @@ const AssignmentSection = ({
|
|||||||
value={gradeField.type}
|
value={gradeField.type}
|
||||||
errorEffort={errorList[`${type}-${gradeField.id}`]}
|
errorEffort={errorList[`${type}-${gradeField.id}`]}
|
||||||
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
|
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
|
||||||
|
viewOnly={viewOnly}
|
||||||
/>
|
/>
|
||||||
<AssignmentItem
|
<AssignmentItem
|
||||||
className="course-grading-assignment-abbreviation"
|
className="course-grading-assignment-abbreviation"
|
||||||
@@ -92,6 +94,7 @@ const AssignmentSection = ({
|
|||||||
name="shortLabel"
|
name="shortLabel"
|
||||||
value={gradeField.shortLabel}
|
value={gradeField.shortLabel}
|
||||||
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
|
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
|
||||||
|
viewOnly={viewOnly}
|
||||||
/>
|
/>
|
||||||
<AssignmentItem
|
<AssignmentItem
|
||||||
className="course-grading-assignment-total-grade"
|
className="course-grading-assignment-total-grade"
|
||||||
@@ -106,6 +109,7 @@ const AssignmentSection = ({
|
|||||||
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
|
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
|
||||||
errorEffort={errorList[`${weight}-${gradeField.id}`]}
|
errorEffort={errorList[`${weight}-${gradeField.id}`]}
|
||||||
trailingElement="%"
|
trailingElement="%"
|
||||||
|
viewOnly={viewOnly}
|
||||||
/>
|
/>
|
||||||
<AssignmentItem
|
<AssignmentItem
|
||||||
className="course-grading-assignment-total-number"
|
className="course-grading-assignment-total-number"
|
||||||
@@ -118,6 +122,7 @@ const AssignmentSection = ({
|
|||||||
value={gradeField.minCount}
|
value={gradeField.minCount}
|
||||||
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
|
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
|
||||||
errorEffort={errorList[`${minCount}-${gradeField.id}`]}
|
errorEffort={errorList[`${minCount}-${gradeField.id}`]}
|
||||||
|
viewOnly={viewOnly}
|
||||||
/>
|
/>
|
||||||
<AssignmentItem
|
<AssignmentItem
|
||||||
className="course-grading-assignment-number-droppable"
|
className="course-grading-assignment-number-droppable"
|
||||||
@@ -134,6 +139,7 @@ const AssignmentSection = ({
|
|||||||
type: gradeField.type,
|
type: gradeField.type,
|
||||||
})}
|
})}
|
||||||
errorEffort={errorList[`${dropCount}-${gradeField.id}`]}
|
errorEffort={errorList[`${dropCount}-${gradeField.id}`]}
|
||||||
|
viewOnly={viewOnly}
|
||||||
/>
|
/>
|
||||||
</ol>
|
</ol>
|
||||||
{showDefinedCaseAlert && (
|
{showDefinedCaseAlert && (
|
||||||
@@ -185,6 +191,7 @@ const AssignmentSection = ({
|
|||||||
variant="outline-primary"
|
variant="outline-primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleRemoveAssignment(gradeField.id)}
|
onClick={() => handleRemoveAssignment(gradeField.id)}
|
||||||
|
disabled={viewOnly}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.assignmentDeleteButton)}
|
{intl.formatMessage(messages.assignmentDeleteButton)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -210,6 +217,7 @@ AssignmentSection.propTypes = {
|
|||||||
graders: PropTypes.arrayOf(
|
graders: PropTypes.arrayOf(
|
||||||
PropTypes.shape(defaultAssignmentsPropTypes),
|
PropTypes.shape(defaultAssignmentsPropTypes),
|
||||||
),
|
),
|
||||||
|
viewOnly: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(AssignmentSection);
|
export default injectIntl(AssignmentSection);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const RootWrapper = (props = {}) => (
|
|||||||
minimumGradeCredit={0.1}
|
minimumGradeCredit={0.1}
|
||||||
setGradingData={jest.fn()}
|
setGradingData={jest.fn()}
|
||||||
setShowSuccessAlert={jest.fn()}
|
setShowSuccessAlert={jest.fn()}
|
||||||
|
viewOnly={false}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
@@ -38,6 +39,14 @@ describe('<CreditSection />', () => {
|
|||||||
expect(inputElement.value).toBe('10');
|
expect(inputElement.value).toBe('10');
|
||||||
fireEvent.change(inputElement, { target: { value: '2' } });
|
fireEvent.change(inputElement, { target: { value: '2' } });
|
||||||
expect(testObj.minimumGradeCredit).toBe(0.02);
|
expect(testObj.minimumGradeCredit).toBe(0.02);
|
||||||
|
expect(inputElement.disabled).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should disable the fields if viewOnly', async () => {
|
||||||
|
const { getByTestId } = render(<RootWrapper viewOnly />);
|
||||||
|
await waitFor(() => {
|
||||||
|
const inputElement = getByTestId('minimum-grade-credit-input');
|
||||||
|
expect(inputElement.disabled).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const CreditSection = ({
|
const CreditSection = ({
|
||||||
intl, eligibleGrade, setShowSavePrompt, minimumGradeCredit, setGradingData, setShowSuccessAlert,
|
intl, eligibleGrade, setShowSavePrompt, minimumGradeCredit, setGradingData, setShowSuccessAlert, viewOnly,
|
||||||
}) => {
|
}) => {
|
||||||
const [errorEffort, setErrorEffort] = useState(false);
|
const [errorEffort, setErrorEffort] = useState(false);
|
||||||
|
|
||||||
@@ -46,6 +46,7 @@ const CreditSection = ({
|
|||||||
value={Math.round(parseFloat(minimumGradeCredit) * 100) || ''}
|
value={Math.round(parseFloat(minimumGradeCredit) * 100) || ''}
|
||||||
name="minimum_grade_credit"
|
name="minimum_grade_credit"
|
||||||
onChange={handleCreditChange}
|
onChange={handleCreditChange}
|
||||||
|
disabled={viewOnly}
|
||||||
/>
|
/>
|
||||||
<Form.Control.Feedback className="grading-description">
|
<Form.Control.Feedback className="grading-description">
|
||||||
{intl.formatMessage(messages.creditEligibilityDescription)}
|
{intl.formatMessage(messages.creditEligibilityDescription)}
|
||||||
@@ -66,6 +67,7 @@ CreditSection.propTypes = {
|
|||||||
setGradingData: PropTypes.func.isRequired,
|
setGradingData: PropTypes.func.isRequired,
|
||||||
setShowSuccessAlert: PropTypes.func.isRequired,
|
setShowSuccessAlert: PropTypes.func.isRequired,
|
||||||
minimumGradeCredit: PropTypes.number.isRequired,
|
minimumGradeCredit: PropTypes.number.isRequired,
|
||||||
|
viewOnly: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(CreditSection);
|
export default injectIntl(CreditSection);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const RootWrapper = (props = {}) => (
|
|||||||
setShowSavePrompt={jest.fn()}
|
setShowSavePrompt={jest.fn()}
|
||||||
setGradingData={jest.fn()}
|
setGradingData={jest.fn()}
|
||||||
setShowSuccessAlert={jest.fn()}
|
setShowSuccessAlert={jest.fn()}
|
||||||
|
viewOnly={false}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
@@ -46,6 +47,7 @@ describe('<DeadlineSection />', () => {
|
|||||||
fireEvent.change(inputElement, { target: { value: '13:13' } });
|
fireEvent.change(inputElement, { target: { value: '13:13' } });
|
||||||
expect(testObj.gracePeriod.hours).toBe(13);
|
expect(testObj.gracePeriod.hours).toBe(13);
|
||||||
expect(testObj.gracePeriod.minutes).toBe(13);
|
expect(testObj.gracePeriod.minutes).toBe(13);
|
||||||
|
expect(inputElement.disabled).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('checking deadline input value if grace Period equal null', async () => {
|
it('checking deadline input value if grace Period equal null', async () => {
|
||||||
@@ -78,4 +80,11 @@ describe('<DeadlineSection />', () => {
|
|||||||
expect(getByText(`Grace period must be specified in ${TIME_FORMAT.toUpperCase()} format.`)).toBeInTheDocument();
|
expect(getByText(`Grace period must be specified in ${TIME_FORMAT.toUpperCase()} format.`)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('checking deadline input is disabled if viewOnly', async () => {
|
||||||
|
const { getByTestId } = render(<RootWrapper viewOnly />);
|
||||||
|
await waitFor(() => {
|
||||||
|
const inputElement = getByTestId('deadline-period-input');
|
||||||
|
expect(inputElement.disabled).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { formatTime, timerValidation } from './utils';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const DeadlineSection = ({
|
const DeadlineSection = ({
|
||||||
intl, setShowSavePrompt, gracePeriod, setGradingData, setShowSuccessAlert,
|
intl, setShowSavePrompt, gracePeriod, setGradingData, setShowSuccessAlert, viewOnly,
|
||||||
}) => {
|
}) => {
|
||||||
const timeStampValue = gracePeriod
|
const timeStampValue = gracePeriod
|
||||||
? gracePeriod.hours && `${formatTime(gracePeriod.hours)}:${formatTime(gracePeriod.minutes)}`
|
? gracePeriod.hours && `${formatTime(gracePeriod.hours)}:${formatTime(gracePeriod.minutes)}`
|
||||||
@@ -52,6 +52,7 @@ const DeadlineSection = ({
|
|||||||
value={newDeadlineValue}
|
value={newDeadlineValue}
|
||||||
onChange={handleDeadlineChange}
|
onChange={handleDeadlineChange}
|
||||||
placeholder={TIME_FORMAT.toUpperCase()}
|
placeholder={TIME_FORMAT.toUpperCase()}
|
||||||
|
disabled={viewOnly}
|
||||||
/>
|
/>
|
||||||
<Form.Control.Feedback className="grading-description">
|
<Form.Control.Feedback className="grading-description">
|
||||||
{intl.formatMessage(messages.gracePeriodOnDeadlineDescription)}
|
{intl.formatMessage(messages.gracePeriodOnDeadlineDescription)}
|
||||||
@@ -78,6 +79,7 @@ DeadlineSection.propTypes = {
|
|||||||
hours: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
hours: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
minutes: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
minutes: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
}),
|
}),
|
||||||
|
viewOnly: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(DeadlineSection);
|
export default injectIntl(DeadlineSection);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const GradingScale = ({
|
|||||||
sortedGrades,
|
sortedGrades,
|
||||||
setOverrideInternetConnectionAlert,
|
setOverrideInternetConnectionAlert,
|
||||||
setEligibleGrade,
|
setEligibleGrade,
|
||||||
|
viewOnly,
|
||||||
}) => {
|
}) => {
|
||||||
const [gradingSegments, setGradingSegments] = useState(sortedGrades);
|
const [gradingSegments, setGradingSegments] = useState(sortedGrades);
|
||||||
const [letters, setLetters] = useState(gradeLetters);
|
const [letters, setLetters] = useState(gradeLetters);
|
||||||
@@ -191,7 +192,7 @@ const GradingScale = ({
|
|||||||
<IconButtonWithTooltip
|
<IconButtonWithTooltip
|
||||||
tooltipPlacement="top"
|
tooltipPlacement="top"
|
||||||
tooltipContent={intl.formatMessage(messages.addNewSegmentButtonAltText)}
|
tooltipContent={intl.formatMessage(messages.addNewSegmentButtonAltText)}
|
||||||
disabled={gradingSegments.length >= 5}
|
disabled={gradingSegments.length >= 5 || viewOnly}
|
||||||
data-testid="grading-scale-btn-add-segment"
|
data-testid="grading-scale-btn-add-segment"
|
||||||
className="mr-3"
|
className="mr-3"
|
||||||
src={IconAdd}
|
src={IconAdd}
|
||||||
@@ -245,6 +246,7 @@ GradingScale.propTypes = {
|
|||||||
}),
|
}),
|
||||||
).isRequired,
|
).isRequired,
|
||||||
setEligibleGrade: PropTypes.func.isRequired,
|
setEligibleGrade: PropTypes.func.isRequired,
|
||||||
|
viewOnly: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(GradingScale);
|
export default injectIntl(GradingScale);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const sortedGrades = [
|
|||||||
{ current: 20, previous: 0 },
|
{ current: 20, previous: 0 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const RootWrapper = () => (
|
const RootWrapper = (viewOnly = { viewOnly: false }) => (
|
||||||
<IntlProvider locale="en" messages={{}}>
|
<IntlProvider locale="en" messages={{}}>
|
||||||
<GradingScale
|
<GradingScale
|
||||||
intl={injectIntl}
|
intl={injectIntl}
|
||||||
@@ -29,6 +29,7 @@ const RootWrapper = () => (
|
|||||||
setGradingData={jest.fn()}
|
setGradingData={jest.fn()}
|
||||||
setOverrideInternetConnectionAlert={jest.fn()}
|
setOverrideInternetConnectionAlert={jest.fn()}
|
||||||
setEligibleGrade={jest.fn()}
|
setEligibleGrade={jest.fn()}
|
||||||
|
{...viewOnly}
|
||||||
/>
|
/>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
);
|
);
|
||||||
@@ -73,6 +74,26 @@ describe('<GradingScale />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not disable new grading segment button when viewOnly=false', async () => {
|
||||||
|
const { getAllByTestId } = render(<RootWrapper />);
|
||||||
|
await waitFor(() => {
|
||||||
|
const addNewSegmentBtn = getAllByTestId('grading-scale-btn-add-segment');
|
||||||
|
expect(addNewSegmentBtn).toHaveLength(1);
|
||||||
|
expect(addNewSegmentBtn[0]).toBeInTheDocument();
|
||||||
|
expect(addNewSegmentBtn[0].disabled).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable new grading segment button when viewOnly', async () => {
|
||||||
|
const { getAllByTestId } = render(<RootWrapper viewOnly />);
|
||||||
|
await waitFor(() => {
|
||||||
|
const addNewSegmentBtn = getAllByTestId('grading-scale-btn-add-segment');
|
||||||
|
expect(addNewSegmentBtn).toHaveLength(1);
|
||||||
|
expect(addNewSegmentBtn[0]).toBeInTheDocument();
|
||||||
|
expect(addNewSegmentBtn[0].disabled).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should remove grading segment when "Remove" button is clicked', async () => {
|
it('should remove grading segment when "Remove" button is clicked', async () => {
|
||||||
const { getAllByTestId } = render(<RootWrapper />);
|
const { getAllByTestId } = render(<RootWrapper />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { StudioHeader } from '@edx/frontend-component-header';
|
import { StudioHeader } from '@edx/frontend-component-header';
|
||||||
import { getContentMenuItems, getSettingMenuItems, getToolsMenuItems } from './utils';
|
import { getContentMenuItems, getSettingMenuItems, getToolsMenuItems } from './utils';
|
||||||
|
import { useUserPermissions } from '../generic/hooks';
|
||||||
|
import { getUserPermissions, getUserPermissionsEnabled } from '../generic/data/selectors';
|
||||||
|
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const Header = ({
|
const Header = ({
|
||||||
@@ -16,24 +20,73 @@ const Header = ({
|
|||||||
// injected
|
// injected
|
||||||
intl,
|
intl,
|
||||||
}) => {
|
}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { checkPermission } = useUserPermissions();
|
||||||
|
const userPermissions = useSelector(getUserPermissions);
|
||||||
|
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||||
|
const hasContentPermissions = !userPermissionsEnabled || (userPermissionsEnabled && checkPermission('manage_content'));
|
||||||
|
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;
|
const studioBaseUrl = getConfig().STUDIO_BASE_URL;
|
||||||
const mainMenuDropdowns = [
|
const contentMenu = getContentMenuItems({
|
||||||
{
|
studioBaseUrl,
|
||||||
id: `${intl.formatMessage(messages['header.links.content'])}-dropdown-menu`,
|
courseId,
|
||||||
buttonTitle: intl.formatMessage(messages['header.links.content']),
|
intl,
|
||||||
items: getContentMenuItems({ studioBaseUrl, courseId, intl }),
|
hasContentPermissions,
|
||||||
},
|
hasOutlinePermissions,
|
||||||
|
});
|
||||||
|
const mainMenuDropdowns = [];
|
||||||
|
const toolsMenu = getToolsMenuItems({
|
||||||
|
studioBaseUrl,
|
||||||
|
courseId,
|
||||||
|
intl,
|
||||||
|
hasToolsPermissions,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchUserPermissionsEnabledFlag());
|
||||||
|
if (!userPermissions) {
|
||||||
|
dispatch(fetchUserPermissionsQuery(courseId));
|
||||||
|
}
|
||||||
|
}, [courseId]);
|
||||||
|
|
||||||
|
if (contentMenu.length > 0) {
|
||||||
|
mainMenuDropdowns.push(
|
||||||
|
{
|
||||||
|
id: `${intl.formatMessage(messages['header.links.content'])}-dropdown-menu`,
|
||||||
|
buttonTitle: intl.formatMessage(messages['header.links.content']),
|
||||||
|
items: contentMenu,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
mainMenuDropdowns.push(
|
||||||
{
|
{
|
||||||
id: `${intl.formatMessage(messages['header.links.settings'])}-dropdown-menu`,
|
id: `${intl.formatMessage(messages['header.links.settings'])}-dropdown-menu`,
|
||||||
buttonTitle: intl.formatMessage(messages['header.links.settings']),
|
buttonTitle: intl.formatMessage(messages['header.links.settings']),
|
||||||
items: getSettingMenuItems({ studioBaseUrl, courseId, intl }),
|
items: getSettingMenuItems({
|
||||||
|
studioBaseUrl,
|
||||||
|
courseId,
|
||||||
|
intl,
|
||||||
|
hasSettingsPermissions,
|
||||||
|
hasAdvancedSettingsAccess,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
);
|
||||||
id: `${intl.formatMessage(messages['header.links.tools'])}-dropdown-menu`,
|
if (toolsMenu.length > 0) {
|
||||||
buttonTitle: intl.formatMessage(messages['header.links.tools']),
|
mainMenuDropdowns.push(
|
||||||
items: getToolsMenuItems({ studioBaseUrl, courseId, intl }),
|
{
|
||||||
},
|
id: `${intl.formatMessage(messages['header.links.tools'])}-dropdown-menu`,
|
||||||
];
|
buttonTitle: intl.formatMessage(messages['header.links.tools']),
|
||||||
|
items: toolsMenu,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const outlineLink = `${studioBaseUrl}/course/${courseId}`;
|
const outlineLink = `${studioBaseUrl}/course/${courseId}`;
|
||||||
return (
|
return (
|
||||||
<StudioHeader
|
<StudioHeader
|
||||||
|
|||||||
@@ -2,72 +2,127 @@ import { getConfig } from '@edx/frontend-platform';
|
|||||||
import { getPagePath } from '../utils';
|
import { getPagePath } from '../utils';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
export const getContentMenuItems = ({ studioBaseUrl, courseId, intl }) => {
|
export const getContentMenuItems = ({
|
||||||
const items = [
|
studioBaseUrl,
|
||||||
{
|
courseId,
|
||||||
href: `${studioBaseUrl}/course/${courseId}`,
|
intl,
|
||||||
title: intl.formatMessage(messages['header.links.outline']),
|
hasContentPermissions,
|
||||||
},
|
hasOutlinePermissions,
|
||||||
{
|
}) => {
|
||||||
href: `${studioBaseUrl}/course_info/${courseId}`,
|
const items = [];
|
||||||
title: intl.formatMessage(messages['header.links.updates']),
|
|
||||||
},
|
if (hasOutlinePermissions) {
|
||||||
{
|
items.push(
|
||||||
href: getPagePath(courseId, 'true', 'tabs'),
|
{
|
||||||
title: intl.formatMessage(messages['header.links.pages']),
|
href: `${studioBaseUrl}/course/${courseId}`,
|
||||||
},
|
title: intl.formatMessage(messages['header.links.outline']),
|
||||||
{
|
},
|
||||||
href: `${studioBaseUrl}/assets/${courseId}`,
|
);
|
||||||
title: intl.formatMessage(messages['header.links.filesAndUploads']),
|
}
|
||||||
},
|
if (hasContentPermissions) {
|
||||||
];
|
items.push(
|
||||||
|
{
|
||||||
|
href: `${studioBaseUrl}/course_info/${courseId}`,
|
||||||
|
title: intl.formatMessage(messages['header.links.updates']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: getPagePath(courseId, 'true', 'tabs'),
|
||||||
|
title: intl.formatMessage(messages['header.links.pages']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: `${studioBaseUrl}/assets/${courseId}`,
|
||||||
|
title: intl.formatMessage(messages['header.links.filesAndUploads']),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
if (getConfig().ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN === 'true') {
|
if (getConfig().ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN === 'true') {
|
||||||
items.push({
|
items.push(
|
||||||
href: `${studioBaseUrl}/videos/${courseId}`,
|
{
|
||||||
title: intl.formatMessage(messages['header.links.videoUploads']),
|
href: `${studioBaseUrl}/videos/${courseId}`,
|
||||||
});
|
title: intl.formatMessage(messages['header.links.videoUploads']),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSettingMenuItems = ({ studioBaseUrl, courseId, intl }) => ([
|
export const getSettingMenuItems = ({
|
||||||
{
|
studioBaseUrl,
|
||||||
href: `${studioBaseUrl}/settings/details/${courseId}`,
|
courseId,
|
||||||
title: intl.formatMessage(messages['header.links.scheduleAndDetails']),
|
intl,
|
||||||
},
|
hasAdvancedSettingsAccess,
|
||||||
{
|
hasSettingsPermissions,
|
||||||
href: `${studioBaseUrl}/settings/grading/${courseId}`,
|
}) => {
|
||||||
title: intl.formatMessage(messages['header.links.grading']),
|
const items = [];
|
||||||
},
|
|
||||||
{
|
|
||||||
href: `${studioBaseUrl}/course_team/${courseId}`,
|
|
||||||
title: intl.formatMessage(messages['header.links.courseTeam']),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: `${studioBaseUrl}/group_configurations/${courseId}`,
|
|
||||||
title: intl.formatMessage(messages['header.links.groupConfigurations']),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: `${studioBaseUrl}/settings/advanced/${courseId}`,
|
|
||||||
title: intl.formatMessage(messages['header.links.advancedSettings']),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: `${studioBaseUrl}/certificates/${courseId}`,
|
|
||||||
title: intl.formatMessage(messages['header.links.certificates']),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const getToolsMenuItems = ({ studioBaseUrl, courseId, intl }) => ([
|
items.push(
|
||||||
{
|
{
|
||||||
href: `${studioBaseUrl}/import/${courseId}`,
|
href: `${studioBaseUrl}/settings/details/${courseId}`,
|
||||||
title: intl.formatMessage(messages['header.links.import']),
|
title: intl.formatMessage(messages['header.links.scheduleAndDetails']),
|
||||||
},
|
},
|
||||||
{
|
);
|
||||||
href: `${studioBaseUrl}/export/${courseId}`,
|
if (hasSettingsPermissions) {
|
||||||
title: intl.formatMessage(messages['header.links.export']),
|
items.push(
|
||||||
}, {
|
{
|
||||||
href: `${studioBaseUrl}/checklists/${courseId}`,
|
href: `${studioBaseUrl}/settings/grading/${courseId}`,
|
||||||
title: intl.formatMessage(messages['header.links.checklists']),
|
title: intl.formatMessage(messages['header.links.grading']),
|
||||||
},
|
},
|
||||||
]);
|
);
|
||||||
|
}
|
||||||
|
items.push(
|
||||||
|
{
|
||||||
|
href: `${studioBaseUrl}/course_team/${courseId}`,
|
||||||
|
title: intl.formatMessage(messages['header.links.courseTeam']),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
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}`,
|
||||||
|
title: intl.formatMessage(messages['header.links.advancedSettings']),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
items.push(
|
||||||
|
{
|
||||||
|
href: `${studioBaseUrl}/certificates/${courseId}`,
|
||||||
|
title: intl.formatMessage(messages['header.links.certificates']),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getToolsMenuItems = ({
|
||||||
|
studioBaseUrl,
|
||||||
|
courseId,
|
||||||
|
intl,
|
||||||
|
hasToolsPermissions,
|
||||||
|
}) => {
|
||||||
|
const items = [];
|
||||||
|
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']),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { getConfig, setConfig } from '@edx/frontend-platform';
|
import { getConfig, setConfig } from '@edx/frontend-platform';
|
||||||
import { getContentMenuItems } from './utils';
|
import { getContentMenuItems, getSettingMenuItems, getToolsMenuItems } from './utils';
|
||||||
|
|
||||||
const props = {
|
const baseProps = {
|
||||||
studioBaseUrl: 'UrLSTuiO',
|
studioBaseUrl: 'UrLSTuiO',
|
||||||
courseId: '123',
|
courseId: '123',
|
||||||
intl: {
|
intl: {
|
||||||
formatMessage: jest.fn(),
|
formatMessage: jest.fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const contentProps = { ...baseProps, hasContentPermissions: true, hasOutlinePermissions: true };
|
||||||
|
const settingProps = { ...baseProps, hasAdvancedSettingsAccess: true, hasSettingsPermissions: true };
|
||||||
|
const toolsProps = { ...baseProps, hasToolsPermissions: true };
|
||||||
|
|
||||||
describe('header utils', () => {
|
describe('header utils', () => {
|
||||||
describe('getContentMenuItems', () => {
|
describe('getContentMenuItems', () => {
|
||||||
@@ -16,7 +19,7 @@ describe('header utils', () => {
|
|||||||
...getConfig(),
|
...getConfig(),
|
||||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: 'true',
|
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: 'true',
|
||||||
});
|
});
|
||||||
const actualItems = getContentMenuItems(props);
|
const actualItems = getContentMenuItems(contentProps);
|
||||||
expect(actualItems).toHaveLength(5);
|
expect(actualItems).toHaveLength(5);
|
||||||
});
|
});
|
||||||
it('should not include Video Uploads option', () => {
|
it('should not include Video Uploads option', () => {
|
||||||
@@ -24,8 +27,84 @@ describe('header utils', () => {
|
|||||||
...getConfig(),
|
...getConfig(),
|
||||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: 'false',
|
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: 'false',
|
||||||
});
|
});
|
||||||
const actualItems = getContentMenuItems(props);
|
const actualItems = getContentMenuItems(contentProps);
|
||||||
expect(actualItems).toHaveLength(4);
|
expect(actualItems).toHaveLength(4);
|
||||||
});
|
});
|
||||||
|
it('should include only Video Uploads option', () => {
|
||||||
|
setConfig({
|
||||||
|
...getConfig(),
|
||||||
|
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: 'true',
|
||||||
|
});
|
||||||
|
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 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', () => {
|
||||||
|
const actualItems = getToolsMenuItems(toolsProps);
|
||||||
|
expect(actualItems).toHaveLength(3);
|
||||||
|
});
|
||||||
|
it('should not include any items if there are no permissions', () => {
|
||||||
|
const actualItems = getToolsMenuItems({ ...baseProps, hasToolsPermissions: false });
|
||||||
|
expect(actualItems).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ import { LAST_IMPORT_COOKIE_NAME } from './data/constants';
|
|||||||
import ImportSidebar from './import-sidebar/ImportSidebar';
|
import ImportSidebar from './import-sidebar/ImportSidebar';
|
||||||
import FileSection from './file-section/FileSection';
|
import FileSection from './file-section/FileSection';
|
||||||
import messages from './messages';
|
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 CourseImportPage = ({ intl, courseId }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -32,6 +35,14 @@ const CourseImportPage = ({ intl, courseId }) => {
|
|||||||
const loadingStatus = useSelector(getLoadingStatus);
|
const loadingStatus = useSelector(getLoadingStatus);
|
||||||
const anyRequestFailed = savingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.FAILED;
|
const anyRequestFailed = savingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.FAILED;
|
||||||
const anyRequestInProgress = savingStatus === RequestStatus.PENDING || loadingStatus === RequestStatus.IN_PROGRESS;
|
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(() => {
|
useEffect(() => {
|
||||||
const cookieData = cookies.get(LAST_IMPORT_COOKIE_NAME);
|
const cookieData = cookies.get(LAST_IMPORT_COOKIE_NAME);
|
||||||
@@ -43,6 +54,12 @@ const CourseImportPage = ({ intl, courseId }) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
if (!hasImportPermissions) {
|
||||||
|
return (
|
||||||
|
<PermissionDeniedAlert />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@@ -72,7 +89,7 @@ const CourseImportPage = ({ intl, courseId }) => {
|
|||||||
<p className="small">{intl.formatMessage(messages.description1)}</p>
|
<p className="small">{intl.formatMessage(messages.description1)}</p>
|
||||||
<p className="small">{intl.formatMessage(messages.description2)}</p>
|
<p className="small">{intl.formatMessage(messages.description2)}</p>
|
||||||
<p className="small">{intl.formatMessage(messages.description3)}</p>
|
<p className="small">{intl.formatMessage(messages.description3)}</p>
|
||||||
<FileSection courseId={courseId} />
|
<FileSection courseId={courseId} viewOnly={viewOnly} />
|
||||||
{importTriggered && <ImportStepper courseId={courseId} />}
|
{importTriggered && <ImportStepper courseId={courseId} />}
|
||||||
</article>
|
</article>
|
||||||
</Layout.Element>
|
</Layout.Element>
|
||||||
|
|||||||
@@ -14,12 +14,17 @@ import CourseImportPage from './CourseImportPage';
|
|||||||
import { getImportStatusApiUrl } from './data/api';
|
import { getImportStatusApiUrl } from './data/api';
|
||||||
import { IMPORT_STAGES } from './data/constants';
|
import { IMPORT_STAGES } from './data/constants';
|
||||||
import stepperMessages from './import-stepper/messages';
|
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 store;
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
let cookies;
|
let cookies;
|
||||||
const courseId = '123';
|
const courseId = '123';
|
||||||
const courseName = 'About Node JS';
|
const courseName = 'About Node JS';
|
||||||
|
const userId = 3;
|
||||||
|
let userPermissionsData = { permissions: [] };
|
||||||
|
|
||||||
jest.mock('../generic/model-store', () => ({
|
jest.mock('../generic/model-store', () => ({
|
||||||
useModel: jest.fn().mockReturnValue({
|
useModel: jest.fn().mockReturnValue({
|
||||||
@@ -47,7 +52,7 @@ describe('<CourseImportPage />', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
initializeMockApp({
|
initializeMockApp({
|
||||||
authenticatedUser: {
|
authenticatedUser: {
|
||||||
userId: 3,
|
userId,
|
||||||
username: 'abc123',
|
username: 'abc123',
|
||||||
administrator: true,
|
administrator: true,
|
||||||
roles: [],
|
roles: [],
|
||||||
@@ -58,6 +63,12 @@ describe('<CourseImportPage />', () => {
|
|||||||
axiosMock
|
axiosMock
|
||||||
.onGet(getImportStatusApiUrl(courseId, 'testFileName.test'))
|
.onGet(getImportStatusApiUrl(courseId, 'testFileName.test'))
|
||||||
.reply(200, { importStatus: 1, message: '' });
|
.reply(200, { importStatus: 1, message: '' });
|
||||||
|
axiosMock
|
||||||
|
.onGet(getUserPermissionsEnabledFlagUrl)
|
||||||
|
.reply(200, { enabled: false });
|
||||||
|
axiosMock
|
||||||
|
.onGet(getUserPermissionsUrl(courseId, userId))
|
||||||
|
.reply(200, userPermissionsData);
|
||||||
cookies = new Cookies();
|
cookies = new Cookies();
|
||||||
cookies.get.mockReturnValue(null);
|
cookies.get.mockReturnValue(null);
|
||||||
});
|
});
|
||||||
@@ -83,6 +94,30 @@ describe('<CourseImportPage />', () => {
|
|||||||
expect(getByText(messages.description3.defaultMessage)).toBeInTheDocument();
|
expect(getByText(messages.description3.defaultMessage)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('should render permissionDenied if incorrect permissions', async () => {
|
||||||
|
const { getByTestId } = render(<RootWrapper />);
|
||||||
|
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||||
|
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||||
|
expect(getByTestId('permissionDeniedAlert')).toBeVisible();
|
||||||
|
});
|
||||||
|
it('should render without errors if correct permissions', async () => {
|
||||||
|
const { getByText } = render(<RootWrapper />);
|
||||||
|
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 () => {
|
it('should fetch status without clicking when cookies has', async () => {
|
||||||
cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.test' });
|
cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.test' });
|
||||||
const { getByText } = render(<RootWrapper />);
|
const { getByText } = render(<RootWrapper />);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
|
FormattedMessage,
|
||||||
injectIntl,
|
injectIntl,
|
||||||
intlShape,
|
intlShape,
|
||||||
} from '@edx/frontend-platform/i18n';
|
} from '@edx/frontend-platform/i18n';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
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 { IMPORT_STAGES } from '../data/constants';
|
||||||
import {
|
import {
|
||||||
getCurrentStage, getError, getFileName, getImportTriggered,
|
getCurrentStage, getError, getFileName, getImportTriggered,
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { handleProcessUpload } from '../data/thunks';
|
import { handleProcessUpload } from '../data/thunks';
|
||||||
|
|
||||||
const FileSection = ({ intl, courseId }) => {
|
const FileSection = ({ intl, courseId, viewOnly }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const importTriggered = useSelector(getImportTriggered);
|
const importTriggered = useSelector(getImportTriggered);
|
||||||
const currentStage = useSelector(getCurrentStage);
|
const currentStage = useSelector(getCurrentStage);
|
||||||
@@ -30,21 +30,25 @@ const FileSection = ({ intl, courseId }) => {
|
|||||||
subtitle={fileName && intl.formatMessage(messages.fileChosen, { fileName })}
|
subtitle={fileName && intl.formatMessage(messages.fileChosen, { fileName })}
|
||||||
/>
|
/>
|
||||||
<Card.Section className="px-3 pt-2 pb-4">
|
<Card.Section className="px-3 pt-2 pb-4">
|
||||||
{isShowedDropzone
|
{!viewOnly && isShowedDropzone && (
|
||||||
&& (
|
<Dropzone
|
||||||
<Dropzone
|
onProcessUpload={
|
||||||
onProcessUpload={
|
({ fileData, requestConfig, handleError }) => dispatch(handleProcessUpload(
|
||||||
({ fileData, requestConfig, handleError }) => dispatch(handleProcessUpload(
|
courseId,
|
||||||
courseId,
|
fileData,
|
||||||
fileData,
|
requestConfig,
|
||||||
requestConfig,
|
handleError,
|
||||||
handleError,
|
))
|
||||||
))
|
}
|
||||||
}
|
accept={{ 'application/gzip': ['.tar.gz'] }}
|
||||||
accept={{ 'application/gzip': ['.tar.gz'] }}
|
data-testid="dropzone"
|
||||||
data-testid="dropzone"
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
{viewOnly && (
|
||||||
|
<Alert variant="info">
|
||||||
|
<FormattedMessage {...messages.viewOnlyAlert} />
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -53,6 +57,7 @@ const FileSection = ({ intl, courseId }) => {
|
|||||||
FileSection.propTypes = {
|
FileSection.propTypes = {
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
courseId: PropTypes.string.isRequired,
|
courseId: PropTypes.string.isRequired,
|
||||||
|
viewOnly: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(FileSection);
|
export default injectIntl(FileSection);
|
||||||
|
|||||||
@@ -14,7 +14,15 @@ const courseId = '123';
|
|||||||
const RootWrapper = () => (
|
const RootWrapper = () => (
|
||||||
<AppProvider store={store}>
|
<AppProvider store={store}>
|
||||||
<IntlProvider locale="en" messages={{}}>
|
<IntlProvider locale="en" messages={{}}>
|
||||||
<FileSection intl={injectIntl} courseId={courseId} />
|
<FileSection intl={injectIntl} courseId={courseId} viewOnly={false} />
|
||||||
|
</IntlProvider>
|
||||||
|
</AppProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const RootWrapperViewOnly = () => (
|
||||||
|
<AppProvider store={store}>
|
||||||
|
<IntlProvider locale="en" messages={{}}>
|
||||||
|
<FileSection intl={injectIntl} courseId={courseId} viewOnly />
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
);
|
);
|
||||||
@@ -27,6 +35,7 @@ describe('<FileSection />', () => {
|
|||||||
username: 'abc123',
|
username: 'abc123',
|
||||||
administrator: true,
|
administrator: true,
|
||||||
roles: [],
|
roles: [],
|
||||||
|
permisions: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
store = initializeStore();
|
store = initializeStore();
|
||||||
@@ -43,6 +52,13 @@ describe('<FileSection />', () => {
|
|||||||
expect(getByTestId('dropzone')).toBeInTheDocument();
|
expect(getByTestId('dropzone')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('should not render Dropzone when view is viewOnly', async () => {
|
||||||
|
const { getByText, queryByTestId, container } = render(<RootWrapperViewOnly />);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryByTestId(container, 'dropzone')).not.toBeInTheDocument();
|
||||||
|
expect(getByText(messages.viewOnlyAlert.defaultMessage)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
it('should work Dropzone', async () => {
|
it('should work Dropzone', async () => {
|
||||||
const {
|
const {
|
||||||
getByText, getByTestId, queryByTestId, container,
|
getByText, getByTestId, queryByTestId, container,
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ const messages = defineMessages({
|
|||||||
id: 'course-authoring.import.file-section.chosen-file',
|
id: 'course-authoring.import.file-section.chosen-file',
|
||||||
defaultMessage: 'File chosen: {fileName}',
|
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;
|
export default messages;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { RequestStatus } from '../data/constants';
|
|||||||
import SettingsComponent from './SettingsComponent';
|
import SettingsComponent from './SettingsComponent';
|
||||||
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
|
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
|
||||||
import getPageHeadTitle from '../generic/utils';
|
import getPageHeadTitle from '../generic/utils';
|
||||||
|
import { useUserPermissions } from '../generic/hooks';
|
||||||
|
|
||||||
const PagesAndResources = ({ courseId, intl }) => {
|
const PagesAndResources = ({ courseId, intl }) => {
|
||||||
const courseDetails = useModel('courseDetails', courseId);
|
const courseDetails = useModel('courseDetails', courseId);
|
||||||
@@ -73,12 +74,14 @@ const PagesAndResources = ({ courseId, intl }) => {
|
|||||||
contentPermissionsPages.push(page);
|
contentPermissionsPages.push(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { checkPermission } = useUserPermissions();
|
||||||
|
|
||||||
if (loadingStatus === RequestStatus.IN_PROGRESS) {
|
if (loadingStatus === RequestStatus.IN_PROGRESS) {
|
||||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (courseAppsApiStatus === RequestStatus.DENIED) {
|
if (courseAppsApiStatus === RequestStatus.DENIED || !checkPermission('manage_content')) {
|
||||||
return (
|
return (
|
||||||
<PermissionDeniedAlert />
|
<PermissionDeniedAlert />
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ import * as xpertUnitSummaryApi from './xpert-unit-summary/data/api';
|
|||||||
|
|
||||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||||
|
|
||||||
|
jest.mock('../generic/hooks', () => ({
|
||||||
|
useUserPermissions: jest.fn(() => ({
|
||||||
|
checkPermission: jest.fn(() => true),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('PagesAndResources', () => {
|
describe('PagesAndResources', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
|||||||
@@ -21,10 +21,13 @@ import scheduleMessages from './schedule-section/messages';
|
|||||||
import genericMessages from '../generic/help-sidebar/messages';
|
import genericMessages from '../generic/help-sidebar/messages';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import ScheduleAndDetails from '.';
|
import ScheduleAndDetails from '.';
|
||||||
|
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api';
|
||||||
|
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
|
||||||
|
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
let store;
|
let store;
|
||||||
const courseId = '123';
|
const courseId = '123';
|
||||||
|
const userId = 3;
|
||||||
|
|
||||||
// Mock the tinymce lib
|
// Mock the tinymce lib
|
||||||
jest.mock('@tinymce/tinymce-react', () => {
|
jest.mock('@tinymce/tinymce-react', () => {
|
||||||
@@ -50,6 +53,18 @@ jest.mock('react-textarea-autosize', () => jest.fn((props) => (
|
|||||||
<textarea {...props} onFocus={() => {}} onBlur={() => {}} />
|
<textarea {...props} onFocus={() => {}} onBlur={() => {}} />
|
||||||
)));
|
)));
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
const RootWrapper = () => (
|
const RootWrapper = () => (
|
||||||
<AppProvider store={store}>
|
<AppProvider store={store}>
|
||||||
<IntlProvider locale="en" messages={{}}>
|
<IntlProvider locale="en" messages={{}}>
|
||||||
@@ -62,7 +77,7 @@ describe('<ScheduleAndDetails />', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
initializeMockApp({
|
initializeMockApp({
|
||||||
authenticatedUser: {
|
authenticatedUser: {
|
||||||
userId: 3,
|
userId,
|
||||||
username: 'abc123',
|
username: 'abc123',
|
||||||
administrator: true,
|
administrator: true,
|
||||||
roles: [],
|
roles: [],
|
||||||
@@ -111,6 +126,30 @@ describe('<ScheduleAndDetails />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should shows the PermissionDeniedAlert when there are not the right user permissions', async () => {
|
||||||
|
const permissionsData = { permissions: ['wrong_permission'] };
|
||||||
|
await permissionsMockStore(permissionsData);
|
||||||
|
|
||||||
|
const { queryByText } = render(<RootWrapper />);
|
||||||
|
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, getAllByText } = render(<RootWrapper />);
|
||||||
|
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.');
|
||||||
|
const scheduleAndDetailElements = getAllByText(messages.headingTitle.defaultMessage);
|
||||||
|
const scheduleAndDetailTitle = scheduleAndDetailElements[0];
|
||||||
|
expect(permissionDeniedAlert).not.toBeInTheDocument();
|
||||||
|
expect(scheduleAndDetailTitle).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should hide credit section with condition', async () => {
|
it('should hide credit section with condition', async () => {
|
||||||
const updatedResponse = {
|
const updatedResponse = {
|
||||||
...courseSettingsMock,
|
...courseSettingsMock,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ module.exports = {
|
|||||||
enrollmentEndEditable: true,
|
enrollmentEndEditable: true,
|
||||||
isCreditCourse: true,
|
isCreditCourse: true,
|
||||||
isEntranceExamsEnabled: true,
|
isEntranceExamsEnabled: true,
|
||||||
|
isEditable: true,
|
||||||
isPrerequisiteCoursesEnabled: true,
|
isPrerequisiteCoursesEnabled: true,
|
||||||
languageOptions: [
|
languageOptions: [
|
||||||
['en', 'English'],
|
['en', 'English'],
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ describe('<DetailsSection />', () => {
|
|||||||
language: courseSettingsMock.languageOptions[1][0],
|
language: courseSettingsMock.languageOptions[1][0],
|
||||||
languageOptions: courseSettingsMock.languageOptions,
|
languageOptions: courseSettingsMock.languageOptions,
|
||||||
onChange: onChangeMock,
|
onChange: onChangeMock,
|
||||||
|
isEditable: courseSettingsMock.isEditable,
|
||||||
};
|
};
|
||||||
|
|
||||||
it('renders details section successfully', () => {
|
it('renders details section successfully', () => {
|
||||||
@@ -57,4 +58,10 @@ describe('<DetailsSection />', () => {
|
|||||||
getByRole('button', { name: messages.dropdownEmpty.defaultMessage }),
|
getByRole('button', { name: messages.dropdownEmpty.defaultMessage }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should disable the dropdown if isEditable is false', () => {
|
||||||
|
const initialProps = { ...props, isEditable: false };
|
||||||
|
const { getByRole } = render(<RootWrapper {...initialProps} />);
|
||||||
|
expect(getByRole('button').disabled).toEqual(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import SectionSubHeader from '../../generic/section-sub-header';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const DetailsSection = ({
|
const DetailsSection = ({
|
||||||
language, languageOptions, onChange,
|
language, languageOptions, onChange, isEditable,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const formattedLanguage = () => {
|
const formattedLanguage = () => {
|
||||||
@@ -24,7 +24,7 @@ const DetailsSection = ({
|
|||||||
<Form.Group className="form-group-custom dropdown-language">
|
<Form.Group className="form-group-custom dropdown-language">
|
||||||
<Form.Label>{intl.formatMessage(messages.dropdownLabel)}</Form.Label>
|
<Form.Label>{intl.formatMessage(messages.dropdownLabel)}</Form.Label>
|
||||||
<Dropdown className="bg-white">
|
<Dropdown className="bg-white">
|
||||||
<Dropdown.Toggle variant="outline-primary" id="languageDropdown">
|
<Dropdown.Toggle variant="outline-primary" id="languageDropdown" disabled={!isEditable}>
|
||||||
{formattedLanguage()}
|
{formattedLanguage()}
|
||||||
</Dropdown.Toggle>
|
</Dropdown.Toggle>
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
@@ -56,6 +56,7 @@ DetailsSection.propTypes = {
|
|||||||
PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
|
PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
|
||||||
).isRequired,
|
).isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
isEditable: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DetailsSection;
|
export default DetailsSection;
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ import LicenseSection from './license-section';
|
|||||||
import ScheduleSidebar from './schedule-sidebar';
|
import ScheduleSidebar from './schedule-sidebar';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { useLoadValuesPrompt, useSaveValuesPrompt } from './hooks';
|
import { useLoadValuesPrompt, useSaveValuesPrompt } from './hooks';
|
||||||
|
import { useUserPermissions } from '../generic/hooks';
|
||||||
|
import { getUserPermissionsEnabled } from '../generic/data/selectors';
|
||||||
|
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
|
||||||
|
|
||||||
const ScheduleAndDetails = ({ intl, courseId }) => {
|
const ScheduleAndDetails = ({ intl, courseId }) => {
|
||||||
const courseSettings = useSelector(getCourseSettings);
|
const courseSettings = useSelector(getCourseSettings);
|
||||||
@@ -53,6 +56,12 @@ const ScheduleAndDetails = ({ intl, courseId }) => {
|
|||||||
|| loadingSettingsStatus === RequestStatus.IN_PROGRESS;
|
|| loadingSettingsStatus === RequestStatus.IN_PROGRESS;
|
||||||
|
|
||||||
const course = useModel('courseDetails', courseId);
|
const course = useModel('courseDetails', courseId);
|
||||||
|
const { checkPermission } = useUserPermissions();
|
||||||
|
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||||
|
const showPermissionDeniedAlert = userPermissionsEnabled && (
|
||||||
|
!checkPermission('manage_course_settings') && !checkPermission('view_course_settings')
|
||||||
|
);
|
||||||
|
const canEdit = (!userPermissionsEnabled) ? true : checkPermission('manage_course_settings');
|
||||||
document.title = getPageHeadTitle(course?.name, intl.formatMessage(messages.headingTitle));
|
document.title = getPageHeadTitle(course?.name, intl.formatMessage(messages.headingTitle));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -145,6 +154,12 @@ const ScheduleAndDetails = ({ intl, courseId }) => {
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showPermissionDeniedAlert) {
|
||||||
|
return (
|
||||||
|
<PermissionDeniedAlert />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (loadingDetailsStatus === RequestStatus.DENIED || loadingSettingsStatus === RequestStatus.DENIED) {
|
if (loadingDetailsStatus === RequestStatus.DENIED || loadingSettingsStatus === RequestStatus.DENIED) {
|
||||||
return (
|
return (
|
||||||
<div className="row justify-content-center m-6">
|
<div className="row justify-content-center m-6">
|
||||||
@@ -266,12 +281,14 @@ const ScheduleAndDetails = ({ intl, courseId }) => {
|
|||||||
certificatesDisplayBehavior={certificatesDisplayBehavior}
|
certificatesDisplayBehavior={certificatesDisplayBehavior}
|
||||||
canShowCertificateAvailableDateField={canShowCertificateAvailableDateField}
|
canShowCertificateAvailableDateField={canShowCertificateAvailableDateField}
|
||||||
onChange={handleValuesChange}
|
onChange={handleValuesChange}
|
||||||
|
isEditable={canEdit}
|
||||||
/>
|
/>
|
||||||
{aboutPageEditable && (
|
{aboutPageEditable && (
|
||||||
<DetailsSection
|
<DetailsSection
|
||||||
language={language}
|
language={language}
|
||||||
languageOptions={languageOptions}
|
languageOptions={languageOptions}
|
||||||
onChange={handleValuesChange}
|
onChange={handleValuesChange}
|
||||||
|
isEditable={canEdit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<IntroducingSection
|
<IntroducingSection
|
||||||
@@ -292,6 +309,7 @@ const ScheduleAndDetails = ({ intl, courseId }) => {
|
|||||||
enableExtendedCourseDetails={enableExtendedCourseDetails}
|
enableExtendedCourseDetails={enableExtendedCourseDetails}
|
||||||
videoThumbnailImageAssetPath={videoThumbnailImageAssetPath}
|
videoThumbnailImageAssetPath={videoThumbnailImageAssetPath}
|
||||||
onChange={handleValuesChange}
|
onChange={handleValuesChange}
|
||||||
|
isEditable={canEdit}
|
||||||
/>
|
/>
|
||||||
{enableExtendedCourseDetails && (
|
{enableExtendedCourseDetails && (
|
||||||
<>
|
<>
|
||||||
@@ -319,12 +337,14 @@ const ScheduleAndDetails = ({ intl, courseId }) => {
|
|||||||
isPrerequisiteCoursesEnabled
|
isPrerequisiteCoursesEnabled
|
||||||
}
|
}
|
||||||
onChange={handleValuesChange}
|
onChange={handleValuesChange}
|
||||||
|
isEditable={canEdit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{licensingEnabled && (
|
{licensingEnabled && (
|
||||||
<LicenseSection
|
<LicenseSection
|
||||||
license={license}
|
license={license}
|
||||||
onChange={handleValuesChange}
|
onChange={handleValuesChange}
|
||||||
|
isEditable={canEdit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const {
|
|||||||
} = courseDetailsMock;
|
} = courseDetailsMock;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
aboutPageEditable, sidebarHtmlEnabled, shortDescriptionEditable, lmsLinkForAboutPage,
|
aboutPageEditable, sidebarHtmlEnabled, shortDescriptionEditable, lmsLinkForAboutPage, isEditable,
|
||||||
} = courseSettingsMock;
|
} = courseSettingsMock;
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
@@ -63,6 +63,7 @@ const props = {
|
|||||||
courseImageAssetPath,
|
courseImageAssetPath,
|
||||||
shortDescriptionEditable,
|
shortDescriptionEditable,
|
||||||
onChange: onChangeMock,
|
onChange: onChangeMock,
|
||||||
|
isEditable,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('<IntroducingSection />', () => {
|
describe('<IntroducingSection />', () => {
|
||||||
@@ -98,4 +99,13 @@ describe('<IntroducingSection />', () => {
|
|||||||
expect(queryAllByText(messages.courseOverviewLabel.defaultMessage).length).toBe(0);
|
expect(queryAllByText(messages.courseOverviewLabel.defaultMessage).length).toBe(0);
|
||||||
expect(queryAllByText(messages.courseAboutSidebarLabel.defaultMessage).length).toBe(0);
|
expect(queryAllByText(messages.courseAboutSidebarLabel.defaultMessage).length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should hide components if isEditable is false', () => {
|
||||||
|
const initialProps = { ...props, isEditable: false };
|
||||||
|
const { queryAllByText } = render(<RootWrapper {...initialProps} />);
|
||||||
|
expect(queryAllByText(messages.introducingTitle.defaultMessage).length).toBe(0);
|
||||||
|
expect(queryAllByText(messages.introducingDescription.defaultMessage).length).toBe(0);
|
||||||
|
expect(queryAllByText(messages.courseOverviewLabel.defaultMessage).length).toBe(0);
|
||||||
|
expect(queryAllByText(messages.courseAboutSidebarLabel.defaultMessage).length).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const IntroducingSection = ({
|
|||||||
enableExtendedCourseDetails,
|
enableExtendedCourseDetails,
|
||||||
videoThumbnailImageAssetPath,
|
videoThumbnailImageAssetPath,
|
||||||
onChange,
|
onChange,
|
||||||
|
isEditable,
|
||||||
}) => {
|
}) => {
|
||||||
const overviewHelpText = (
|
const overviewHelpText = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@@ -72,7 +73,7 @@ const IntroducingSection = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="section-container introducing-section">
|
<section className="section-container introducing-section">
|
||||||
{aboutPageEditable && (
|
{aboutPageEditable && isEditable && (
|
||||||
<SectionSubHeader
|
<SectionSubHeader
|
||||||
title={intl.formatMessage(messages.introducingTitle)}
|
title={intl.formatMessage(messages.introducingTitle)}
|
||||||
description={intl.formatMessage(messages.introducingDescription)}
|
description={intl.formatMessage(messages.introducingDescription)}
|
||||||
@@ -87,7 +88,7 @@ const IntroducingSection = ({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{shortDescriptionEditable && (
|
{shortDescriptionEditable && isEditable && (
|
||||||
<Form.Group className="form-group-custom">
|
<Form.Group className="form-group-custom">
|
||||||
<Form.Label>
|
<Form.Label>
|
||||||
{intl.formatMessage(messages.courseShortDescriptionLabel)}
|
{intl.formatMessage(messages.courseShortDescriptionLabel)}
|
||||||
@@ -107,7 +108,7 @@ const IntroducingSection = ({
|
|||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
)}
|
)}
|
||||||
{aboutPageEditable && (
|
{aboutPageEditable && isEditable && (
|
||||||
<>
|
<>
|
||||||
<Form.Group className="form-group-custom">
|
<Form.Group className="form-group-custom">
|
||||||
<Form.Label>{intl.formatMessage(messages.courseOverviewLabel)}</Form.Label>
|
<Form.Label>{intl.formatMessage(messages.courseOverviewLabel)}</Form.Label>
|
||||||
@@ -160,7 +161,7 @@ const IntroducingSection = ({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{aboutPageEditable && (
|
{aboutPageEditable && isEditable && (
|
||||||
<IntroductionVideo introVideo={introVideo} onChange={onChange} />
|
<IntroductionVideo introVideo={introVideo} onChange={onChange} />
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
@@ -200,6 +201,7 @@ IntroducingSection.propTypes = {
|
|||||||
enableExtendedCourseDetails: PropTypes.bool.isRequired,
|
enableExtendedCourseDetails: PropTypes.bool.isRequired,
|
||||||
videoThumbnailImageAssetPath: PropTypes.string,
|
videoThumbnailImageAssetPath: PropTypes.string,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
isEditable: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default injectIntl(IntroducingSection);
|
export default injectIntl(IntroducingSection);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { courseDetailsMock } from '../__mocks__';
|
import { courseDetailsMock, courseSettingsMock } from '../__mocks__';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import LicenseSection from '.';
|
import LicenseSection from '.';
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ const RootWrapper = (props) => (
|
|||||||
const props = {
|
const props = {
|
||||||
license: courseDetailsMock.license,
|
license: courseDetailsMock.license,
|
||||||
onChange: onChangeMock,
|
onChange: onChangeMock,
|
||||||
|
isEditable: courseSettingsMock.isEditable,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('<LicenseSection />', () => {
|
describe('<LicenseSection />', () => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { LICENSE_TYPE } from './constants';
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { useLicenseDetails } from './hooks';
|
import { useLicenseDetails } from './hooks';
|
||||||
|
|
||||||
const LicenseSection = ({ license, onChange }) => {
|
const LicenseSection = ({ license, onChange, isEditable }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
licenseURL,
|
licenseURL,
|
||||||
@@ -29,6 +29,7 @@ const LicenseSection = ({ license, onChange }) => {
|
|||||||
<LicenseSelector
|
<LicenseSelector
|
||||||
licenseType={licenseType}
|
licenseType={licenseType}
|
||||||
onChangeLicenseType={handleChangeLicenseType}
|
onChangeLicenseType={handleChangeLicenseType}
|
||||||
|
isEditable={isEditable}
|
||||||
/>
|
/>
|
||||||
{licenseType === LICENSE_TYPE.creativeCommons && (
|
{licenseType === LICENSE_TYPE.creativeCommons && (
|
||||||
<LicenseCommonsOptions
|
<LicenseCommonsOptions
|
||||||
@@ -52,6 +53,7 @@ LicenseSection.defaultProps = {
|
|||||||
LicenseSection.propTypes = {
|
LicenseSection.propTypes = {
|
||||||
license: PropTypes.string,
|
license: PropTypes.string,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
isEditable: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LicenseSection;
|
export default LicenseSection;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const RootWrapper = (props) => (
|
|||||||
const props = {
|
const props = {
|
||||||
licenseType: LICENSE_TYPE.allRightsReserved,
|
licenseType: LICENSE_TYPE.allRightsReserved,
|
||||||
onChangeLicenseType: onChangeLicenseTypeMock,
|
onChangeLicenseType: onChangeLicenseTypeMock,
|
||||||
|
isEditable: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('<LicenseSelector />', () => {
|
describe('<LicenseSelector />', () => {
|
||||||
@@ -60,4 +61,13 @@ describe('<LicenseSelector />', () => {
|
|||||||
expect(buttonFirst).toHaveClass('btn btn-outline-primary');
|
expect(buttonFirst).toHaveClass('btn btn-outline-primary');
|
||||||
expect(buttonSecond).toHaveClass('btn btn-outline-primary');
|
expect(buttonSecond).toHaveClass('btn btn-outline-primary');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show disabled buttons if isEditable is false', () => {
|
||||||
|
const initialProps = { ...props, isEditable: false };
|
||||||
|
const { getByRole } = render(<RootWrapper {...initialProps} />);
|
||||||
|
const buttonFirst = getByRole('button', { name: messages.licenseChoice1.defaultMessage });
|
||||||
|
const buttonSecond = getByRole('button', { name: messages.licenseChoice2.defaultMessage });
|
||||||
|
expect(buttonFirst.disabled).toEqual(true);
|
||||||
|
expect(buttonSecond.disabled).toEqual(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { LICENSE_TYPE } from '../constants';
|
import { LICENSE_TYPE } from '../constants';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
const LicenseSelector = ({ licenseType, onChangeLicenseType }) => {
|
const LicenseSelector = ({ licenseType, onChangeLicenseType, isEditable }) => {
|
||||||
const LICENSE_BUTTON_GROUP_LABELS = {
|
const LICENSE_BUTTON_GROUP_LABELS = {
|
||||||
[LICENSE_TYPE.allRightsReserved]: {
|
[LICENSE_TYPE.allRightsReserved]: {
|
||||||
label: <FormattedMessage {...messages.licenseChoice1} />,
|
label: <FormattedMessage {...messages.licenseChoice1} />,
|
||||||
@@ -37,6 +37,7 @@ const LicenseSelector = ({ licenseType, onChangeLicenseType }) => {
|
|||||||
<Button
|
<Button
|
||||||
variant={isActive ? 'primary' : 'outline-primary'}
|
variant={isActive ? 'primary' : 'outline-primary'}
|
||||||
onClick={() => onChangeLicenseType(type, 'license')}
|
onClick={() => onChangeLicenseType(type, 'license')}
|
||||||
|
disabled={!isEditable}
|
||||||
>
|
>
|
||||||
{LICENSE_BUTTON_GROUP_LABELS[type].label}
|
{LICENSE_BUTTON_GROUP_LABELS[type].label}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -64,6 +65,7 @@ LicenseSelector.defaultProps = {
|
|||||||
LicenseSelector.propTypes = {
|
LicenseSelector.propTypes = {
|
||||||
licenseType: PropTypes.oneOf(Object.values(LICENSE_TYPE)),
|
licenseType: PropTypes.oneOf(Object.values(LICENSE_TYPE)),
|
||||||
onChangeLicenseType: PropTypes.func.isRequired,
|
onChangeLicenseType: PropTypes.func.isRequired,
|
||||||
|
isEditable: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LicenseSelector;
|
export default LicenseSelector;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const {
|
|||||||
isEntranceExamsEnabled,
|
isEntranceExamsEnabled,
|
||||||
possiblePreRequisiteCourses,
|
possiblePreRequisiteCourses,
|
||||||
isPrerequisiteCoursesEnabled,
|
isPrerequisiteCoursesEnabled,
|
||||||
|
isEditable,
|
||||||
} = courseSettingsMock;
|
} = courseSettingsMock;
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
@@ -49,6 +50,7 @@ const props = {
|
|||||||
entranceExamMinimumScorePct,
|
entranceExamMinimumScorePct,
|
||||||
isPrerequisiteCoursesEnabled,
|
isPrerequisiteCoursesEnabled,
|
||||||
onChange: onChangeMock,
|
onChange: onChangeMock,
|
||||||
|
isEditable,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('<RequirementsSection />', () => {
|
describe('<RequirementsSection />', () => {
|
||||||
@@ -90,4 +92,10 @@ describe('<RequirementsSection />', () => {
|
|||||||
expect(queryAllByLabelText(messages.dropdownLabel.defaultMessage).length).toBe(0);
|
expect(queryAllByLabelText(messages.dropdownLabel.defaultMessage).length).toBe(0);
|
||||||
expect(queryAllByLabelText(entranceExamMessages.requirementsEntrance.defaultMessage).length).toBe(0);
|
expect(queryAllByLabelText(entranceExamMessages.requirementsEntrance.defaultMessage).length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should disable the dropdown if isEditable is false', () => {
|
||||||
|
const initialProps = { ...props, isEditable: false };
|
||||||
|
const { queryByTestId } = render(<RootWrapper {...initialProps} />);
|
||||||
|
expect(queryByTestId('prerequisite_dropdown').disabled).toEqual(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { courseDetailsMock } from '../../__mocks__';
|
import { courseDetailsMock, courseSettingsMock } from '../../__mocks__';
|
||||||
import gradeRequirementsMessages from '../grade-requirements/messages';
|
import gradeRequirementsMessages from '../grade-requirements/messages';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import EntranceExam from '.';
|
import EntranceExam from '.';
|
||||||
@@ -30,6 +30,7 @@ const props = {
|
|||||||
isCheckedString: courseDetailsMock.entranceExamEnabled,
|
isCheckedString: courseDetailsMock.entranceExamEnabled,
|
||||||
entranceExamMinimumScorePct: courseDetailsMock.entranceExamMinimumScorePct,
|
entranceExamMinimumScorePct: courseDetailsMock.entranceExamMinimumScorePct,
|
||||||
onChange: onChangeMock,
|
onChange: onChangeMock,
|
||||||
|
isEditable: courseSettingsMock.isEditable,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('<EntranceExam />', () => {
|
describe('<EntranceExam />', () => {
|
||||||
@@ -58,4 +59,11 @@ describe('<EntranceExam />', () => {
|
|||||||
).toBe(0);
|
).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should disable the checkbox if isEditable is false', () => {
|
||||||
|
const initialProps = { ...props, isEditable: false };
|
||||||
|
const { getAllByRole } = render(<RootWrapper {...initialProps} />);
|
||||||
|
const checkbox = getAllByRole('checkbox')[0];
|
||||||
|
expect(checkbox.disabled).toEqual(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const EntranceExam = ({
|
|||||||
isCheckedString,
|
isCheckedString,
|
||||||
entranceExamMinimumScorePct,
|
entranceExamMinimumScorePct,
|
||||||
onChange,
|
onChange,
|
||||||
|
isEditable,
|
||||||
}) => {
|
}) => {
|
||||||
const { courseId } = useParams();
|
const { courseId } = useParams();
|
||||||
const showEntranceExam = isCheckedString === 'true';
|
const showEntranceExam = isCheckedString === 'true';
|
||||||
@@ -33,6 +34,7 @@ const EntranceExam = ({
|
|||||||
<Form.Checkbox
|
<Form.Checkbox
|
||||||
checked={showEntranceExam}
|
checked={showEntranceExam}
|
||||||
onChange={toggleEntranceExam}
|
onChange={toggleEntranceExam}
|
||||||
|
disabled={!isEditable}
|
||||||
>
|
>
|
||||||
<FormattedMessage {...messages.requirementsEntranceCollapseTitle} />
|
<FormattedMessage {...messages.requirementsEntranceCollapseTitle} />
|
||||||
</Form.Checkbox>
|
</Form.Checkbox>
|
||||||
@@ -63,6 +65,7 @@ const EntranceExam = ({
|
|||||||
errorEffort={errorEffort}
|
errorEffort={errorEffort}
|
||||||
entranceExamMinimumScorePct={entranceExamMinimumScorePct}
|
entranceExamMinimumScorePct={entranceExamMinimumScorePct}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
isEditable={isEditable}
|
||||||
/>
|
/>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</>
|
</>
|
||||||
@@ -83,6 +86,7 @@ EntranceExam.propTypes = {
|
|||||||
isCheckedString: PropTypes.string,
|
isCheckedString: PropTypes.string,
|
||||||
entranceExamMinimumScorePct: PropTypes.string,
|
entranceExamMinimumScorePct: PropTypes.string,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
isEditable: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EntranceExam;
|
export default EntranceExam;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { courseDetailsMock } from '../../__mocks__';
|
import { courseDetailsMock, courseSettingsMock } from '../../__mocks__';
|
||||||
import scheduleMessage from '../../messages';
|
import scheduleMessage from '../../messages';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import GradeRequirements from '.';
|
import GradeRequirements from '.';
|
||||||
@@ -21,6 +21,7 @@ const props = {
|
|||||||
errorEffort: '',
|
errorEffort: '',
|
||||||
entranceExamMinimumScorePct: courseDetailsMock.entranceExamMinimumScorePct,
|
entranceExamMinimumScorePct: courseDetailsMock.entranceExamMinimumScorePct,
|
||||||
onChange: onChangeMock,
|
onChange: onChangeMock,
|
||||||
|
isEditable: courseSettingsMock.isEditable,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('<GradeRequirements />', () => {
|
describe('<GradeRequirements />', () => {
|
||||||
@@ -31,6 +32,13 @@ describe('<GradeRequirements />', () => {
|
|||||||
expect(getByDisplayValue(props.entranceExamMinimumScorePct)).toBeInTheDocument();
|
expect(getByDisplayValue(props.entranceExamMinimumScorePct)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('disable the input if isEditable is false', () => {
|
||||||
|
const initialProps = { ...props, isEditable: false };
|
||||||
|
const { getByDisplayValue } = render(<RootWrapper {...initialProps} />);
|
||||||
|
const input = getByDisplayValue(props.entranceExamMinimumScorePct);
|
||||||
|
expect(input.disabled).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('should call onChange on input change', () => {
|
it('should call onChange on input change', () => {
|
||||||
const { getByDisplayValue } = render(<RootWrapper {...props} />);
|
const { getByDisplayValue } = render(<RootWrapper {...props} />);
|
||||||
const input = getByDisplayValue(props.entranceExamMinimumScorePct);
|
const input = getByDisplayValue(props.entranceExamMinimumScorePct);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const GradeRequirements = ({
|
|||||||
errorEffort,
|
errorEffort,
|
||||||
entranceExamMinimumScorePct,
|
entranceExamMinimumScorePct,
|
||||||
onChange,
|
onChange,
|
||||||
|
isEditable,
|
||||||
}) => (
|
}) => (
|
||||||
<Form.Group
|
<Form.Group
|
||||||
className={classNames('form-group-custom', {
|
className={classNames('form-group-custom', {
|
||||||
@@ -27,6 +28,7 @@ const GradeRequirements = ({
|
|||||||
value={entranceExamMinimumScorePct}
|
value={entranceExamMinimumScorePct}
|
||||||
onChange={(e) => onChange(e.target.value, 'entranceExamMinimumScorePct')}
|
onChange={(e) => onChange(e.target.value, 'entranceExamMinimumScorePct')}
|
||||||
trailingElement="%"
|
trailingElement="%"
|
||||||
|
disabled={!isEditable}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
{errorEffort && (
|
{errorEffort && (
|
||||||
@@ -49,6 +51,7 @@ GradeRequirements.propTypes = {
|
|||||||
errorEffort: PropTypes.string,
|
errorEffort: PropTypes.string,
|
||||||
entranceExamMinimumScorePct: PropTypes.string,
|
entranceExamMinimumScorePct: PropTypes.string,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
isEditable: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GradeRequirements;
|
export default GradeRequirements;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const RequirementsSection = ({
|
|||||||
entranceExamMinimumScorePct,
|
entranceExamMinimumScorePct,
|
||||||
isPrerequisiteCoursesEnabled,
|
isPrerequisiteCoursesEnabled,
|
||||||
onChange,
|
onChange,
|
||||||
|
isEditable,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const selectedItem = possiblePreRequisiteCourses?.find(
|
const selectedItem = possiblePreRequisiteCourses?.find(
|
||||||
@@ -33,7 +34,7 @@ const RequirementsSection = ({
|
|||||||
>
|
>
|
||||||
<Form.Label>{intl.formatMessage(messages.dropdownLabel)}</Form.Label>
|
<Form.Label>{intl.formatMessage(messages.dropdownLabel)}</Form.Label>
|
||||||
<Dropdown className="bg-white">
|
<Dropdown className="bg-white">
|
||||||
<Dropdown.Toggle id="prerequisiteDropdown" variant="outline-primary">
|
<Dropdown.Toggle id="prerequisiteDropdown" variant="outline-primary" disabled={!isEditable} data-testid="prerequisite_dropdown">
|
||||||
{formattedSelectedItem}
|
{formattedSelectedItem}
|
||||||
</Dropdown.Toggle>
|
</Dropdown.Toggle>
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
@@ -74,6 +75,7 @@ const RequirementsSection = ({
|
|||||||
value={effort || ''}
|
value={effort || ''}
|
||||||
placeholder={TIME_FORMAT.toUpperCase()}
|
placeholder={TIME_FORMAT.toUpperCase()}
|
||||||
onChange={(e) => onChange(e.target.value, 'effort')}
|
onChange={(e) => onChange(e.target.value, 'effort')}
|
||||||
|
disabled={!isEditable}
|
||||||
/>
|
/>
|
||||||
<Form.Control.Feedback>
|
<Form.Control.Feedback>
|
||||||
{intl.formatMessage(messages.timepickerHelpText)}
|
{intl.formatMessage(messages.timepickerHelpText)}
|
||||||
@@ -87,6 +89,7 @@ const RequirementsSection = ({
|
|||||||
isCheckedString={entranceExamEnabled}
|
isCheckedString={entranceExamEnabled}
|
||||||
entranceExamMinimumScorePct={entranceExamMinimumScorePct}
|
entranceExamMinimumScorePct={entranceExamMinimumScorePct}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
isEditable={isEditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
@@ -125,6 +128,7 @@ RequirementsSection.propTypes = {
|
|||||||
entranceExamMinimumScorePct: PropTypes.string,
|
entranceExamMinimumScorePct: PropTypes.string,
|
||||||
isPrerequisiteCoursesEnabled: PropTypes.bool.isRequired,
|
isPrerequisiteCoursesEnabled: PropTypes.bool.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
isEditable: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RequirementsSection;
|
export default RequirementsSection;
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ const CertificateDisplayRow = ({
|
|||||||
<Form.Label>
|
<Form.Label>
|
||||||
{intl.formatMessage(messages.certificateBehaviorLabel)}
|
{intl.formatMessage(messages.certificateBehaviorLabel)}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Dropdown claswsName="bg-white">
|
<Dropdown className="bg-white">
|
||||||
<Dropdown.Toggle id="certificate-behavior-dropdown" variant="outline-primary">
|
<Dropdown.Toggle id="certificate-behavior-dropdown" variant="outline-primary">
|
||||||
{certificateDisplayValue}
|
{certificateDisplayValue}
|
||||||
</Dropdown.Toggle>
|
</Dropdown.Toggle>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const ScheduleSection = ({
|
|||||||
certificatesDisplayBehavior,
|
certificatesDisplayBehavior,
|
||||||
canShowCertificateAvailableDateField,
|
canShowCertificateAvailableDateField,
|
||||||
onChange,
|
onChange,
|
||||||
|
isEditable,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const enrollmentEndHelpText = intl.formatMessage(
|
const enrollmentEndHelpText = intl.formatMessage(
|
||||||
@@ -42,6 +43,7 @@ const ScheduleSection = ({
|
|||||||
],
|
],
|
||||||
rowType: SCHEDULE_ROW_TYPES.datetime,
|
rowType: SCHEDULE_ROW_TYPES.datetime,
|
||||||
helpText: intl.formatMessage(messages.scheduleCourseStartDateHelpText),
|
helpText: intl.formatMessage(messages.scheduleCourseStartDateHelpText),
|
||||||
|
readonly: !isEditable,
|
||||||
controlName: 'startDate',
|
controlName: 'startDate',
|
||||||
errorFeedback: errorFields?.startDate,
|
errorFeedback: errorFields?.startDate,
|
||||||
},
|
},
|
||||||
@@ -53,6 +55,7 @@ const ScheduleSection = ({
|
|||||||
value: endDate,
|
value: endDate,
|
||||||
rowType: SCHEDULE_ROW_TYPES.datetime,
|
rowType: SCHEDULE_ROW_TYPES.datetime,
|
||||||
helpText: intl.formatMessage(messages.scheduleCourseEndDateHelpText),
|
helpText: intl.formatMessage(messages.scheduleCourseEndDateHelpText),
|
||||||
|
readonly: !isEditable,
|
||||||
controlName: 'endDate',
|
controlName: 'endDate',
|
||||||
errorFeedback: errorFields?.endDate,
|
errorFeedback: errorFields?.endDate,
|
||||||
},
|
},
|
||||||
@@ -73,6 +76,7 @@ const ScheduleSection = ({
|
|||||||
value: enrollmentStart,
|
value: enrollmentStart,
|
||||||
rowType: SCHEDULE_ROW_TYPES.datetime,
|
rowType: SCHEDULE_ROW_TYPES.datetime,
|
||||||
helpText: intl.formatMessage(messages.scheduleEnrollmentStartDateHelpText),
|
helpText: intl.formatMessage(messages.scheduleEnrollmentStartDateHelpText),
|
||||||
|
readonly: !isEditable,
|
||||||
controlName: 'enrollmentStart',
|
controlName: 'enrollmentStart',
|
||||||
errorFeedback: errorFields?.enrollmentStart,
|
errorFeedback: errorFields?.enrollmentStart,
|
||||||
},
|
},
|
||||||
@@ -84,7 +88,7 @@ const ScheduleSection = ({
|
|||||||
value: enrollmentEnd,
|
value: enrollmentEnd,
|
||||||
rowType: SCHEDULE_ROW_TYPES.datetime,
|
rowType: SCHEDULE_ROW_TYPES.datetime,
|
||||||
helpText: computedEnrollmentEndHelpText,
|
helpText: computedEnrollmentEndHelpText,
|
||||||
readonly: !enrollmentEndEditable,
|
readonly: !enrollmentEndEditable || !isEditable,
|
||||||
controlName: 'enrollmentEnd',
|
controlName: 'enrollmentEnd',
|
||||||
errorFeedback: errorFields?.enrollmentEnd,
|
errorFeedback: errorFields?.enrollmentEnd,
|
||||||
},
|
},
|
||||||
@@ -165,6 +169,7 @@ ScheduleSection.propTypes = {
|
|||||||
certificatesDisplayBehavior: PropTypes.string.isRequired,
|
certificatesDisplayBehavior: PropTypes.string.isRequired,
|
||||||
canShowCertificateAvailableDateField: PropTypes.bool.isRequired,
|
canShowCertificateAvailableDateField: PropTypes.bool.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
isEditable: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ScheduleSection;
|
export default ScheduleSection;
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ const StudioHome = ({ intl }) => {
|
|||||||
<SubHeader
|
<SubHeader
|
||||||
title={intl.formatMessage(messages.headingTitle, { studioShortName: studioShortName || 'Studio' })}
|
title={intl.formatMessage(messages.headingTitle, { studioShortName: studioShortName || 'Studio' })}
|
||||||
headerActions={headerButtons}
|
headerActions={headerButtons}
|
||||||
|
key={studioShortName}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ describe('studio-home api calls', () => {
|
|||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
fit('should get studio courses data', async () => {
|
it('should get studio courses data', async () => {
|
||||||
const apiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/courses`;
|
const apiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/courses`;
|
||||||
axiosMock.onGet(apiLink).reply(200, generateGetStudioCoursesApiResponse());
|
axiosMock.onGet(apiLink).reply(200, generateGetStudioCoursesApiResponse());
|
||||||
const result = await getStudioHomeCourses('');
|
const result = await getStudioHomeCourses('');
|
||||||
|
|||||||
Reference in New Issue
Block a user