feat: Add permissions checks for group_configuration, grading, outline (#829)
* feat: update header options for access control, course outline access checks, grade-settings access checks, and view only for grading page
This commit is contained in:
@@ -41,6 +41,9 @@ import DeleteModal from './delete-modal/DeleteModal';
|
||||
import PageAlerts from './page-alerts/PageAlerts';
|
||||
import { useCourseOutline } from './hooks';
|
||||
import messages from './messages';
|
||||
import { useUserPermissions } from '../generic/hooks';
|
||||
import { getUserPermissionsEnabled } from '../generic/data/selectors';
|
||||
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
|
||||
|
||||
const CourseOutline = ({ courseId }) => {
|
||||
const intl = useIntl();
|
||||
@@ -108,6 +111,12 @@ const CourseOutline = ({ courseId }) => {
|
||||
|
||||
const [sections, setSections] = useState(sectionsList);
|
||||
|
||||
const { checkPermission } = useUserPermissions();
|
||||
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||
const hasOutlinePermissions = !userPermissionsEnabled || (
|
||||
userPermissionsEnabled && (checkPermission('manage_libraries') || checkPermission('manage_content'))
|
||||
);
|
||||
|
||||
let initialSections = [...sectionsList];
|
||||
|
||||
const {
|
||||
@@ -241,6 +250,12 @@ const CourseOutline = ({ courseId }) => {
|
||||
setSections(sectionsList);
|
||||
}, [sectionsList]);
|
||||
|
||||
if (!hasOutlinePermissions) {
|
||||
return (
|
||||
<PermissionDeniedAlert />
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return (
|
||||
|
||||
@@ -48,10 +48,15 @@ import pasteButtonMessages from './paste-button/messages';
|
||||
import subsectionMessages from './subsection-card/messages';
|
||||
import pageAlertMessages from './page-alerts/messages';
|
||||
|
||||
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api';
|
||||
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
const courseId = '123';
|
||||
const userId = 3;
|
||||
const userPermissionsData = { permissions: [] };
|
||||
|
||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||
|
||||
@@ -90,7 +95,7 @@ describe('<CourseOutline />', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
userId,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
@@ -102,6 +107,14 @@ describe('<CourseOutline />', () => {
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineIndexApiUrl(courseId))
|
||||
.reply(200, courseOutlineIndexMock);
|
||||
axiosMock
|
||||
.onGet(getUserPermissionsEnabledFlagUrl)
|
||||
.reply(200, { enabled: false });
|
||||
axiosMock
|
||||
.onGet(getUserPermissionsUrl(courseId, userId))
|
||||
.reply(200, userPermissionsData);
|
||||
executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch);
|
||||
});
|
||||
|
||||
@@ -114,6 +127,13 @@ describe('<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 () => {
|
||||
const { findByText, findByTestId } = render(<RootWrapper />);
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ import CreditSection from './credit-section';
|
||||
import DeadlineSection from './deadline-section';
|
||||
import { useConvertGradeCutoffs, useUpdateGradingData } from './hooks';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import { useUserPermissions } from '../generic/hooks';
|
||||
import { getUserPermissionsEnabled } from '../generic/data/selectors';
|
||||
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
|
||||
|
||||
const GradingSettings = ({ intl, courseId }) => {
|
||||
const gradingSettingsData = useSelector(getGradingSettings);
|
||||
@@ -43,6 +46,14 @@ const GradingSettings = ({ intl, courseId }) => {
|
||||
const [isQueryPending, setIsQueryPending] = useState(false);
|
||||
const [showOverrideInternetConnectionAlert, setOverrideInternetConnectionAlert] = useState(false);
|
||||
const [eligibleGrade, setEligibleGrade] = useState(null);
|
||||
const { checkPermission } = useUserPermissions();
|
||||
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||
const hasGradingPermissions = !userPermissionsEnabled || (
|
||||
userPermissionsEnabled && (checkPermission('manage_course_settings') || checkPermission('view_course_settings'))
|
||||
);
|
||||
const viewOnly = !userPermissionsEnabled || (
|
||||
userPermissionsEnabled && checkPermission('view_course_settings') && !checkPermission('manage_course_settings')
|
||||
);
|
||||
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
|
||||
@@ -83,6 +94,12 @@ const GradingSettings = ({ intl, courseId }) => {
|
||||
dispatch(fetchCourseSettingsQuery(courseId));
|
||||
}, [courseId]);
|
||||
|
||||
if (!hasGradingPermissions) {
|
||||
return (
|
||||
<PermissionDeniedAlert />
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
@@ -156,6 +173,7 @@ const GradingSettings = ({ intl, courseId }) => {
|
||||
resetDataRef={resetDataRef}
|
||||
setOverrideInternetConnectionAlert={setOverrideInternetConnectionAlert}
|
||||
setEligibleGrade={setEligibleGrade}
|
||||
viewOnly={viewOnly}
|
||||
/>
|
||||
</section>
|
||||
{courseSettingsData.creditEligibilityEnabled && courseSettingsData.isCreditCourse && (
|
||||
@@ -170,6 +188,7 @@ const GradingSettings = ({ intl, courseId }) => {
|
||||
minimumGradeCredit={minimumGradeCredit}
|
||||
setGradingData={setGradingData}
|
||||
setShowSuccessAlert={setShowSuccessAlert}
|
||||
viewOnly={viewOnly}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
@@ -183,6 +202,7 @@ const GradingSettings = ({ intl, courseId }) => {
|
||||
gracePeriod={gracePeriod}
|
||||
setGradingData={setGradingData}
|
||||
setShowSuccessAlert={setShowSuccessAlert}
|
||||
viewOnly={viewOnly}
|
||||
/>
|
||||
</section>
|
||||
<section>
|
||||
@@ -201,11 +221,13 @@ const GradingSettings = ({ intl, courseId }) => {
|
||||
setGradingData={setGradingData}
|
||||
courseAssignmentLists={courseAssignmentLists}
|
||||
setShowSuccessAlert={setShowSuccessAlert}
|
||||
viewOnly={viewOnly}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
iconBefore={IconAdd}
|
||||
onClick={handleAddAssignment}
|
||||
disabled={viewOnly}
|
||||
>
|
||||
{intl.formatMessage(messages.addNewAssignmentTypeBtn)}
|
||||
</Button>
|
||||
|
||||
@@ -12,9 +12,15 @@ import gradingSettings from './__mocks__/gradingSettings';
|
||||
import GradingSettings from './GradingSettings';
|
||||
import messages from './messages';
|
||||
|
||||
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api';
|
||||
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
|
||||
import { executeThunk } from '../utils';
|
||||
|
||||
const courseId = '123';
|
||||
const userId = 3;
|
||||
let axiosMock;
|
||||
let store;
|
||||
const userPermissionsData = { permissions: [] };
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
@@ -28,7 +34,7 @@ describe('<GradingSettings />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
userId,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
@@ -40,6 +46,25 @@ describe('<GradingSettings />', () => {
|
||||
axiosMock
|
||||
.onGet(getGradingSettingsApiUrl(courseId))
|
||||
.reply(200, gradingSettings);
|
||||
axiosMock
|
||||
.onGet(getUserPermissionsEnabledFlagUrl)
|
||||
.reply(200, { enabled: false });
|
||||
axiosMock
|
||||
.onGet(getUserPermissionsUrl(courseId, userId))
|
||||
.reply(200, userPermissionsData);
|
||||
executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock
|
||||
.onGet(getUserPermissionsEnabledFlagUrl)
|
||||
.reply(200, { enabled: false });
|
||||
axiosMock
|
||||
.onGet(getUserPermissionsUrl(courseId, userId))
|
||||
.reply(200, userPermissionsData);
|
||||
executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
});
|
||||
|
||||
it('should render without errors', async () => {
|
||||
@@ -54,6 +79,38 @@ describe('<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 () => {
|
||||
const { getByTestId, getAllByTestId } = render(<RootWrapper />);
|
||||
await waitFor(() => {
|
||||
@@ -76,6 +133,7 @@ describe('<GradingSettings />', () => {
|
||||
expect(segmentInput).toHaveValue('a');
|
||||
});
|
||||
});
|
||||
|
||||
it('should save segment input changes and display saving message', async () => {
|
||||
const { getByText, getAllByTestId } = render(<RootWrapper />);
|
||||
await waitFor(() => {
|
||||
@@ -88,4 +146,23 @@ describe('<GradingSettings />', () => {
|
||||
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,
|
||||
gradeField,
|
||||
trailingElement,
|
||||
viewOnly,
|
||||
}) => (
|
||||
<li className={className}>
|
||||
<Form.Group className={classNames('form-group-custom', {
|
||||
@@ -37,6 +38,7 @@ const AssignmentItem = ({
|
||||
value={value}
|
||||
isInvalid={errorEffort}
|
||||
trailingElement={trailingElement}
|
||||
disabled={viewOnly}
|
||||
/>
|
||||
<Form.Control.Feedback className="grading-description">
|
||||
{descriptions}
|
||||
@@ -81,6 +83,7 @@ AssignmentItem.propTypes = {
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
gradeField: PropTypes.shape(defaultAssignmentsPropTypes),
|
||||
trailingElement: PropTypes.string,
|
||||
viewOnly: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default AssignmentItem;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ASSIGNMENT_TYPES, DUPLICATE_ASSIGNMENT_NAME } from '../utils/enum';
|
||||
import messages from '../messages';
|
||||
|
||||
const AssignmentTypeName = ({
|
||||
intl, value, errorEffort, onChange,
|
||||
intl, value, errorEffort, onChange, viewOnly,
|
||||
}) => {
|
||||
const initialAssignmentName = useRef(value);
|
||||
|
||||
@@ -28,6 +28,7 @@ const AssignmentTypeName = ({
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
isInvalid={Boolean(errorEffort)}
|
||||
disabled={viewOnly}
|
||||
/>
|
||||
<Form.Control.Feedback className="grading-description">
|
||||
{intl.formatMessage(messages.assignmentTypeNameDescription)}
|
||||
@@ -64,6 +65,7 @@ AssignmentTypeName.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
errorEffort: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).isRequired,
|
||||
viewOnly: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AssignmentTypeName);
|
||||
|
||||
@@ -22,6 +22,7 @@ const AssignmentSection = ({
|
||||
setGradingData,
|
||||
courseAssignmentLists,
|
||||
setShowSuccessAlert,
|
||||
viewOnly,
|
||||
}) => {
|
||||
const [errorList, setErrorList] = useState({});
|
||||
const {
|
||||
@@ -83,6 +84,7 @@ const AssignmentSection = ({
|
||||
value={gradeField.type}
|
||||
errorEffort={errorList[`${type}-${gradeField.id}`]}
|
||||
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
|
||||
viewOnly={viewOnly}
|
||||
/>
|
||||
<AssignmentItem
|
||||
className="course-grading-assignment-abbreviation"
|
||||
@@ -92,6 +94,7 @@ const AssignmentSection = ({
|
||||
name="shortLabel"
|
||||
value={gradeField.shortLabel}
|
||||
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
|
||||
viewOnly={viewOnly}
|
||||
/>
|
||||
<AssignmentItem
|
||||
className="course-grading-assignment-total-grade"
|
||||
@@ -106,6 +109,7 @@ const AssignmentSection = ({
|
||||
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
|
||||
errorEffort={errorList[`${weight}-${gradeField.id}`]}
|
||||
trailingElement="%"
|
||||
viewOnly={viewOnly}
|
||||
/>
|
||||
<AssignmentItem
|
||||
className="course-grading-assignment-total-number"
|
||||
@@ -118,6 +122,7 @@ const AssignmentSection = ({
|
||||
value={gradeField.minCount}
|
||||
onChange={(e) => handleAssignmentChange(e, gradeField.id)}
|
||||
errorEffort={errorList[`${minCount}-${gradeField.id}`]}
|
||||
viewOnly={viewOnly}
|
||||
/>
|
||||
<AssignmentItem
|
||||
className="course-grading-assignment-number-droppable"
|
||||
@@ -134,6 +139,7 @@ const AssignmentSection = ({
|
||||
type: gradeField.type,
|
||||
})}
|
||||
errorEffort={errorList[`${dropCount}-${gradeField.id}`]}
|
||||
viewOnly={viewOnly}
|
||||
/>
|
||||
</ol>
|
||||
{showDefinedCaseAlert && (
|
||||
@@ -185,6 +191,7 @@ const AssignmentSection = ({
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveAssignment(gradeField.id)}
|
||||
disabled={viewOnly}
|
||||
>
|
||||
{intl.formatMessage(messages.assignmentDeleteButton)}
|
||||
</Button>
|
||||
@@ -210,6 +217,7 @@ AssignmentSection.propTypes = {
|
||||
graders: PropTypes.arrayOf(
|
||||
PropTypes.shape(defaultAssignmentsPropTypes),
|
||||
),
|
||||
viewOnly: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AssignmentSection);
|
||||
|
||||
@@ -18,6 +18,7 @@ const RootWrapper = (props = {}) => (
|
||||
minimumGradeCredit={0.1}
|
||||
setGradingData={jest.fn()}
|
||||
setShowSuccessAlert={jest.fn()}
|
||||
viewOnly={false}
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>
|
||||
@@ -38,6 +39,14 @@ describe('<CreditSection />', () => {
|
||||
expect(inputElement.value).toBe('10');
|
||||
fireEvent.change(inputElement, { target: { value: '2' } });
|
||||
expect(testObj.minimumGradeCredit).toBe(0.02);
|
||||
expect(inputElement.disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
it('should disable the fields if viewOnly', async () => {
|
||||
const { getByTestId } = render(<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';
|
||||
|
||||
const CreditSection = ({
|
||||
intl, eligibleGrade, setShowSavePrompt, minimumGradeCredit, setGradingData, setShowSuccessAlert,
|
||||
intl, eligibleGrade, setShowSavePrompt, minimumGradeCredit, setGradingData, setShowSuccessAlert, viewOnly,
|
||||
}) => {
|
||||
const [errorEffort, setErrorEffort] = useState(false);
|
||||
|
||||
@@ -46,6 +46,7 @@ const CreditSection = ({
|
||||
value={Math.round(parseFloat(minimumGradeCredit) * 100) || ''}
|
||||
name="minimum_grade_credit"
|
||||
onChange={handleCreditChange}
|
||||
disabled={viewOnly}
|
||||
/>
|
||||
<Form.Control.Feedback className="grading-description">
|
||||
{intl.formatMessage(messages.creditEligibilityDescription)}
|
||||
@@ -66,6 +67,7 @@ CreditSection.propTypes = {
|
||||
setGradingData: PropTypes.func.isRequired,
|
||||
setShowSuccessAlert: PropTypes.func.isRequired,
|
||||
minimumGradeCredit: PropTypes.number.isRequired,
|
||||
viewOnly: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CreditSection);
|
||||
|
||||
@@ -22,6 +22,7 @@ const RootWrapper = (props = {}) => (
|
||||
setShowSavePrompt={jest.fn()}
|
||||
setGradingData={jest.fn()}
|
||||
setShowSuccessAlert={jest.fn()}
|
||||
viewOnly={false}
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>
|
||||
@@ -46,6 +47,7 @@ describe('<DeadlineSection />', () => {
|
||||
fireEvent.change(inputElement, { target: { value: '13:13' } });
|
||||
expect(testObj.gracePeriod.hours).toBe(13);
|
||||
expect(testObj.gracePeriod.minutes).toBe(13);
|
||||
expect(inputElement.disabled).toEqual(false);
|
||||
});
|
||||
});
|
||||
it('checking deadline input value if grace Period equal null', async () => {
|
||||
@@ -78,4 +80,11 @@ describe('<DeadlineSection />', () => {
|
||||
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';
|
||||
|
||||
const DeadlineSection = ({
|
||||
intl, setShowSavePrompt, gracePeriod, setGradingData, setShowSuccessAlert,
|
||||
intl, setShowSavePrompt, gracePeriod, setGradingData, setShowSuccessAlert, viewOnly,
|
||||
}) => {
|
||||
const timeStampValue = gracePeriod
|
||||
? gracePeriod.hours && `${formatTime(gracePeriod.hours)}:${formatTime(gracePeriod.minutes)}`
|
||||
@@ -52,6 +52,7 @@ const DeadlineSection = ({
|
||||
value={newDeadlineValue}
|
||||
onChange={handleDeadlineChange}
|
||||
placeholder={TIME_FORMAT.toUpperCase()}
|
||||
disabled={viewOnly}
|
||||
/>
|
||||
<Form.Control.Feedback className="grading-description">
|
||||
{intl.formatMessage(messages.gracePeriodOnDeadlineDescription)}
|
||||
@@ -78,6 +79,7 @@ DeadlineSection.propTypes = {
|
||||
hours: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
minutes: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
}),
|
||||
viewOnly: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(DeadlineSection);
|
||||
|
||||
@@ -23,6 +23,7 @@ const GradingScale = ({
|
||||
sortedGrades,
|
||||
setOverrideInternetConnectionAlert,
|
||||
setEligibleGrade,
|
||||
viewOnly,
|
||||
}) => {
|
||||
const [gradingSegments, setGradingSegments] = useState(sortedGrades);
|
||||
const [letters, setLetters] = useState(gradeLetters);
|
||||
@@ -191,7 +192,7 @@ const GradingScale = ({
|
||||
<IconButtonWithTooltip
|
||||
tooltipPlacement="top"
|
||||
tooltipContent={intl.formatMessage(messages.addNewSegmentButtonAltText)}
|
||||
disabled={gradingSegments.length >= 5}
|
||||
disabled={gradingSegments.length >= 5 || viewOnly}
|
||||
data-testid="grading-scale-btn-add-segment"
|
||||
className="mr-3"
|
||||
src={IconAdd}
|
||||
@@ -245,6 +246,7 @@ GradingScale.propTypes = {
|
||||
}),
|
||||
).isRequired,
|
||||
setEligibleGrade: PropTypes.func.isRequired,
|
||||
viewOnly: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(GradingScale);
|
||||
|
||||
@@ -16,7 +16,7 @@ const sortedGrades = [
|
||||
{ current: 20, previous: 0 },
|
||||
];
|
||||
|
||||
const RootWrapper = () => (
|
||||
const RootWrapper = (viewOnly = { viewOnly: false }) => (
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<GradingScale
|
||||
intl={injectIntl}
|
||||
@@ -29,6 +29,7 @@ const RootWrapper = () => (
|
||||
setGradingData={jest.fn()}
|
||||
setOverrideInternetConnectionAlert={jest.fn()}
|
||||
setEligibleGrade={jest.fn()}
|
||||
{...viewOnly}
|
||||
/>
|
||||
</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 () => {
|
||||
const { getAllByTestId } = render(<RootWrapper />);
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -25,8 +25,11 @@ const Header = ({
|
||||
const userPermissions = useSelector(getUserPermissions);
|
||||
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||
const hasContentPermissions = !userPermissionsEnabled || (userPermissionsEnabled && checkPermission('manage_content'));
|
||||
const hasSettingsPermissions = !userPermissionsEnabled
|
||||
const hasOutlinePermissions = !userPermissionsEnabled || (userPermissionsEnabled && (checkPermission('manage_content') || checkPermission('manage_libraries')));
|
||||
const hasAdvancedSettingsAccess = !userPermissionsEnabled
|
||||
|| (userPermissionsEnabled && (checkPermission('manage_advanced_settings') || checkPermission('view_course_settings')));
|
||||
const hasSettingsPermissions = !userPermissionsEnabled
|
||||
|| (userPermissionsEnabled && (checkPermission('manage_course_settings') || checkPermission('view_course_settings')));
|
||||
const hasToolsPermissions = !userPermissionsEnabled
|
||||
|| (userPermissionsEnabled && (checkPermission('manage_course_settings') || checkPermission('view_course_settings')));
|
||||
const studioBaseUrl = getConfig().STUDIO_BASE_URL;
|
||||
@@ -35,6 +38,7 @@ const Header = ({
|
||||
courseId,
|
||||
intl,
|
||||
hasContentPermissions,
|
||||
hasOutlinePermissions,
|
||||
});
|
||||
const mainMenuDropdowns = [];
|
||||
const toolsMenu = getToolsMenuItems({
|
||||
@@ -69,6 +73,7 @@ const Header = ({
|
||||
courseId,
|
||||
intl,
|
||||
hasSettingsPermissions,
|
||||
hasAdvancedSettingsAccess,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -7,15 +7,20 @@ export const getContentMenuItems = ({
|
||||
courseId,
|
||||
intl,
|
||||
hasContentPermissions,
|
||||
hasOutlinePermissions,
|
||||
}) => {
|
||||
const items = [];
|
||||
|
||||
if (hasContentPermissions) {
|
||||
if (hasOutlinePermissions) {
|
||||
items.push(
|
||||
{
|
||||
href: `${studioBaseUrl}/course/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.outline']),
|
||||
},
|
||||
);
|
||||
}
|
||||
if (hasContentPermissions) {
|
||||
items.push(
|
||||
{
|
||||
href: `${studioBaseUrl}/course_info/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.updates']),
|
||||
@@ -31,11 +36,12 @@ export const getContentMenuItems = ({
|
||||
);
|
||||
}
|
||||
if (getConfig().ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN === 'true') {
|
||||
items.push({
|
||||
href: `${studioBaseUrl}/videos/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.videoUploads']),
|
||||
}
|
||||
);
|
||||
items.push(
|
||||
{
|
||||
href: `${studioBaseUrl}/videos/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.videoUploads']),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
@@ -45,6 +51,7 @@ export const getSettingMenuItems = ({
|
||||
studioBaseUrl,
|
||||
courseId,
|
||||
intl,
|
||||
hasAdvancedSettingsAccess,
|
||||
hasSettingsPermissions,
|
||||
}) => {
|
||||
const items = [];
|
||||
@@ -54,20 +61,30 @@ export const getSettingMenuItems = ({
|
||||
href: `${studioBaseUrl}/settings/details/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.scheduleAndDetails']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/settings/grading/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.grading']),
|
||||
},
|
||||
);
|
||||
if (hasSettingsPermissions) {
|
||||
items.push(
|
||||
{
|
||||
href: `${studioBaseUrl}/settings/grading/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.grading']),
|
||||
},
|
||||
);
|
||||
}
|
||||
items.push(
|
||||
{
|
||||
href: `${studioBaseUrl}/course_team/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.courseTeam']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/group_configurations/course-v1:${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.groupConfigurations']),
|
||||
},
|
||||
);
|
||||
if (hasSettingsPermissions) {
|
||||
items.push(
|
||||
{
|
||||
href: `${studioBaseUrl}/group_configurations/course-v1:${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.groupConfigurations']),
|
||||
},
|
||||
);
|
||||
}
|
||||
if (hasAdvancedSettingsAccess) {
|
||||
items.push(
|
||||
{
|
||||
href: `${studioBaseUrl}/settings/advanced/${courseId}`,
|
||||
|
||||
@@ -8,8 +8,8 @@ const baseProps = {
|
||||
formatMessage: jest.fn(),
|
||||
},
|
||||
};
|
||||
const contentProps = { ...baseProps, hasContentPermissions: true };
|
||||
const settingProps = { ...baseProps, hasSettingsPermissions: true };
|
||||
const contentProps = { ...baseProps, hasContentPermissions: true, hasOutlinePermissions: true };
|
||||
const settingProps = { ...baseProps, hasAdvancedSettingsAccess: true, hasSettingsPermissions: true };
|
||||
const toolsProps = { ...baseProps, hasToolsPermissions: true };
|
||||
|
||||
describe('header utils', () => {
|
||||
@@ -35,19 +35,67 @@ describe('header utils', () => {
|
||||
...getConfig(),
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: 'true',
|
||||
});
|
||||
const actualItems = getContentMenuItems({ ...baseProps, hasContentPermissions: false });
|
||||
const actualItems = getContentMenuItems({
|
||||
...baseProps,
|
||||
hasContentPermissions: false,
|
||||
hasOutlinePermissions: false,
|
||||
});
|
||||
expect(actualItems).toHaveLength(1);
|
||||
});
|
||||
it('should include Outline if outline permissions', () => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: 'false',
|
||||
});
|
||||
const actualItems = getContentMenuItems({
|
||||
...baseProps,
|
||||
hasContentPermissions: false,
|
||||
hasOutlinePermissions: true,
|
||||
});
|
||||
expect(actualItems).toHaveLength(1);
|
||||
});
|
||||
it('should include content sections if content permissions', () => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: 'false',
|
||||
});
|
||||
const actualItems = getContentMenuItems({
|
||||
...baseProps,
|
||||
hasContentPermissions: true,
|
||||
hasOutlinePermissions: false,
|
||||
});
|
||||
expect(actualItems).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
describe('getSettingMenuItems', async () => {
|
||||
it('should include all options', () => {
|
||||
const actualItems = getSettingMenuItems(settingProps);
|
||||
expect(actualItems).toHaveLength(6);
|
||||
});
|
||||
it('should not include Advanced Settings option', () => {
|
||||
const actualItems = getSettingMenuItems({ ...baseProps, hasSettingsPermissions: false });
|
||||
it('should include Advanced Settings option, but not settings options', () => {
|
||||
const actualItems = getSettingMenuItems({
|
||||
...baseProps,
|
||||
hasSettingsPermissions: false,
|
||||
hasAdvancedSettingsAccess: true,
|
||||
});
|
||||
expect(actualItems).toHaveLength(4);
|
||||
});
|
||||
it('should include settings, but not advanced settings', () => {
|
||||
const actualItems = getSettingMenuItems({
|
||||
...baseProps,
|
||||
hasSettingsPermissions: true,
|
||||
hasAdvancedSettingsAccess: false,
|
||||
});
|
||||
expect(actualItems).toHaveLength(5);
|
||||
});
|
||||
it('should only include default options', () => {
|
||||
const actualItems = getSettingMenuItems({
|
||||
...baseProps,
|
||||
hasSettingsPermissions: false,
|
||||
hasAdvancedSettingsAccess: false,
|
||||
});
|
||||
expect(actualItems).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
describe('getToolsMenuItems', async () => {
|
||||
it('should include all options', () => {
|
||||
|
||||
Reference in New Issue
Block a user