feat: [ROLES-26] Helper function for ingesting permission data (#670)
* feat: Add UserPermissions api, specs, feature flag api
This commit is contained in:
@@ -92,7 +92,7 @@
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"axios-mock-adapter": "1.22.0",
|
||||
"glob": "7.2.3",
|
||||
"husky": "7.0.4",
|
||||
"husky": "^7.0.4",
|
||||
"jest-canvas-mock": "^2.5.2",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"react-test-renderer": "17.0.2",
|
||||
|
||||
@@ -18,12 +18,11 @@ import AddUserForm from './add-user-form/AddUserForm';
|
||||
import AddTeamMember from './add-team-member/AddTeamMember';
|
||||
import CourseTeamMember from './course-team-member/CourseTeamMember';
|
||||
import InfoModal from './info-modal/InfoModal';
|
||||
import { useCourseTeam } from './hooks';
|
||||
import { useCourseTeam, useUserPermissions } from './hooks';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
|
||||
const CourseTeam = ({ courseId }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
|
||||
|
||||
@@ -55,6 +54,12 @@ const CourseTeam = ({ courseId }) => {
|
||||
handleInternetConnectionFailed,
|
||||
} = useCourseTeam({ intl, courseId });
|
||||
|
||||
const {
|
||||
hasPermissions,
|
||||
} = useUserPermissions();
|
||||
|
||||
const hasManageAllUsersPerm = hasPermissions('manage_all_users');
|
||||
|
||||
if (isLoading) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
@@ -77,7 +82,7 @@ const CourseTeam = ({ courseId }) => {
|
||||
<SubHeader
|
||||
title={intl.formatMessage(messages.headingTitle)}
|
||||
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
||||
headerActions={isAllowActions && (
|
||||
headerActions={(isAllowActions || hasManageAllUsersPerm) && (
|
||||
<Button
|
||||
variant="primary"
|
||||
iconBefore={IconAdd}
|
||||
@@ -104,13 +109,13 @@ const CourseTeam = ({ courseId }) => {
|
||||
role={role}
|
||||
email={email}
|
||||
currentUserEmail={currentUserEmail || ''}
|
||||
isAllowActions={isAllowActions}
|
||||
isAllowActions={isAllowActions || hasManageAllUsersPerm}
|
||||
isHideActions={role === USER_ROLES.admin && isSingleAdmin}
|
||||
onChangeRole={handleChangeRoleUserSubmit}
|
||||
onDelete={handleOpenDeleteModal}
|
||||
/>
|
||||
)) : null}
|
||||
{isShowAddTeamMember && (
|
||||
{(isShowAddTeamMember || hasManageAllUsersPerm) && (
|
||||
<AddTeamMember
|
||||
onFormOpen={openForm}
|
||||
isButtonDisable={isFormVisible}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import initializeStore from '../store';
|
||||
import { courseTeamMock, courseTeamWithOneUser, courseTeamWithoutUsers } from './__mocks__';
|
||||
import { getCourseTeamApiUrl, updateCourseTeamUserApiUrl } from './data/api';
|
||||
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api';
|
||||
import CourseTeam from './CourseTeam';
|
||||
import messages from './messages';
|
||||
import { USER_ROLES } from '../constants';
|
||||
@@ -24,6 +25,8 @@ let axiosMock;
|
||||
let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
const courseId = '123';
|
||||
const userId = 3;
|
||||
const userPermissionsData = { permissions: ['manage_all_users'] };
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
@@ -44,7 +47,7 @@ describe('<CourseTeam />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
userId,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
@@ -53,6 +56,12 @@ describe('<CourseTeam />', () => {
|
||||
|
||||
store = initializeStore();
|
||||
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 () => {
|
||||
@@ -165,13 +174,13 @@ describe('<CourseTeam />', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
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);
|
||||
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 or hasManageAllUsersPerm is false', async () => {
|
||||
cleanup();
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
@@ -179,6 +188,9 @@ describe('<CourseTeam />', () => {
|
||||
...courseTeamWithOneUser,
|
||||
allowActions: false,
|
||||
});
|
||||
axiosMock
|
||||
.onGet(getUserPermissionsEnabledFlagUrl)
|
||||
.reply(200, { enabled: false });
|
||||
|
||||
const { queryByRole, queryByTestId } = render(<RootWrapper />);
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ const CourseTeamMember = ({
|
||||
<div className="course-team-member" data-testid="course-team-member">
|
||||
<div className="member-info">
|
||||
<Badge className={`badge-current-user bg-${badgeColor} text-light-100`}>
|
||||
{isAdminRole
|
||||
{(isAdminRole)
|
||||
? intl.formatMessage(messages.roleAdmin)
|
||||
: intl.formatMessage(messages.roleStaff)}
|
||||
{currentUserEmail === email && (
|
||||
@@ -46,11 +46,13 @@ const CourseTeamMember = ({
|
||||
!isHideActions ? (
|
||||
<div className="member-actions">
|
||||
<Button
|
||||
variant={isAdminRole ? 'tertiary' : 'primary'}
|
||||
variant={(isAdminRole) ? 'tertiary' : 'primary'}
|
||||
size="sm"
|
||||
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>
|
||||
<IconButtonWithTooltip
|
||||
src={DeleteOutline}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useToggle } from '@edx/paragon';
|
||||
import { USER_ROLES } from '../constants';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { useModel } from '../generic/model-store';
|
||||
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
|
||||
import { getUserPermissions, getUserPermissionsEnabled } from '../generic/data/selectors';
|
||||
import {
|
||||
changeRoleTeamUserQuery,
|
||||
createCourseTeamQuery,
|
||||
@@ -96,6 +98,8 @@ const useCourseTeam = ({ courseId }) => {
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCourseTeamQuery(courseId));
|
||||
dispatch(fetchUserPermissionsEnabledFlag());
|
||||
dispatch(fetchUserPermissionsQuery(courseId));
|
||||
}, [courseId]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -135,5 +139,20 @@ const useCourseTeam = ({ courseId }) => {
|
||||
};
|
||||
};
|
||||
|
||||
const useUserPermissions = () => {
|
||||
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||
const userPermissions = useSelector(getUserPermissions);
|
||||
const hasPermissions = (checkPermissions) => {
|
||||
if (userPermissionsEnabled) {
|
||||
return userPermissions?.includes(checkPermissions);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
hasPermissions,
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { useCourseTeam };
|
||||
export { useCourseTeam, useUserPermissions };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-check
|
||||
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';
|
||||
|
||||
@@ -8,7 +8,8 @@ export const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
export const getCreateOrRerunCourseUrl = () => new URL('course/', 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 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.
|
||||
* @returns {Promise<string[]>}
|
||||
@@ -43,3 +44,23 @@ export async function createOrRerunCourse(courseData) {
|
||||
);
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user course roles permissions.
|
||||
* @param {string} courseId
|
||||
* @param {string} userId
|
||||
* @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,
|
||||
getCourseRerunUrl,
|
||||
getCourseRerun,
|
||||
getUserPermissions,
|
||||
getUserPermissionsUrl,
|
||||
getUserPermissionsEnabledFlag,
|
||||
getUserPermissionsEnabledFlagUrl,
|
||||
} from './api';
|
||||
|
||||
let axiosMock;
|
||||
const courseId = 'course-123';
|
||||
|
||||
describe('generic api calls', () => {
|
||||
beforeEach(() => {
|
||||
@@ -22,6 +27,8 @@ describe('generic api calls', () => {
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
userPermissions: [],
|
||||
userPermissionsEnabled: false,
|
||||
});
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
@@ -41,7 +48,6 @@ describe('generic api calls', () => {
|
||||
});
|
||||
|
||||
it('should get course rerun', async () => {
|
||||
const courseId = 'course-mock-id';
|
||||
const courseRerunData = {
|
||||
allowUnicodeCourseId: false,
|
||||
courseCreatorStatus: 'granted',
|
||||
@@ -72,4 +78,24 @@ describe('generic api calls', () => {
|
||||
expect(axiosMock.history.post[0].url).toEqual(getCreateOrRerunCourseUrl());
|
||||
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 getRedirectUrlObj = (state) => state.generic.createOrRerunCourse.redirectUrlObj;
|
||||
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: {
|
||||
organizationLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
courseRerunLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
userPermissionsLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
userPermissionsEnabledLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
},
|
||||
savingStatus: '',
|
||||
organizations: [],
|
||||
@@ -18,6 +20,8 @@ const slice = createSlice({
|
||||
redirectUrlObj: {},
|
||||
postErrors: {},
|
||||
},
|
||||
userPermissions: [],
|
||||
userPermissionsEnabled: false,
|
||||
},
|
||||
reducers: {
|
||||
fetchOrganizations: (state, { payload }) => {
|
||||
@@ -41,6 +45,12 @@ const slice = createSlice({
|
||||
updatePostErrors: (state, { payload }) => {
|
||||
state.createOrRerunCourse.postErrors = payload;
|
||||
},
|
||||
updateUserPermissions: (state, { payload }) => {
|
||||
state.userPermissions = payload;
|
||||
},
|
||||
updateUserPermissionsEnabled: (state, { payload }) => {
|
||||
state.userPermissionsEnabled = payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -52,6 +62,8 @@ export const {
|
||||
updateSavingStatus,
|
||||
updateCourseData,
|
||||
updateRedirectUrlObj,
|
||||
updateUserPermissions,
|
||||
updateUserPermissionsEnabled,
|
||||
} = slice.actions;
|
||||
|
||||
export const {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { createOrRerunCourse, getOrganizations, getCourseRerun } from './api';
|
||||
import {
|
||||
createOrRerunCourse, getOrganizations, getCourseRerun, getUserPermissions, getUserPermissionsEnabledFlag,
|
||||
} from './api';
|
||||
import {
|
||||
fetchOrganizations,
|
||||
updatePostErrors,
|
||||
@@ -7,6 +9,8 @@ import {
|
||||
updateRedirectUrlObj,
|
||||
updateCourseRerunData,
|
||||
updateSavingStatus,
|
||||
updateUserPermissions,
|
||||
updateUserPermissionsEnabled,
|
||||
} from './slice';
|
||||
|
||||
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 }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user