Skip to content

Commit 7324297

Browse files
authored
Merge pull request #62 from open-craft/kshitij/use-custom-grade-range-redwood
feat: Use configured DEFAULT_GRADE_DESIGNATIONS
2 parents d4f6606 + acbd7fa commit 7324297

File tree

10 files changed

+232
-268
lines changed

10 files changed

+232
-268
lines changed

src/grading-settings/GradingSettings.jsx

Lines changed: 53 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,59 @@
1-
import React, { useEffect, useState } from 'react';
2-
import { useDispatch, useSelector } from 'react-redux';
3-
import PropTypes from 'prop-types';
4-
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
1+
import { useIntl } from '@edx/frontend-platform/i18n';
52
import {
6-
Container, Layout, Button, StatefulButton,
3+
Button, Container, Layout, StatefulButton,
74
} from '@openedx/paragon';
8-
import { CheckCircle, Warning, Add as IconAdd } from '@openedx/paragon/icons';
9-
10-
import { useModel } from '../generic/model-store';
5+
import { Add as IconAdd, CheckCircle, Warning } from '@openedx/paragon/icons';
6+
import {
7+
useCourseSettings,
8+
useGradingSettings,
9+
useGradingSettingUpdater,
10+
} from 'CourseAuthoring/grading-settings/data/apiHooks';
11+
import PropTypes from 'prop-types';
12+
import React, { useEffect, useState } from 'react';
13+
import { Helmet } from 'react-helmet';
14+
import { STATEFUL_BUTTON_STATES } from '../constants';
1115
import AlertMessage from '../generic/alert-message';
12-
import { RequestStatus } from '../data/constants';
1316
import InternetConnectionAlert from '../generic/internet-connection-alert';
14-
import SubHeader from '../generic/sub-header/SubHeader';
17+
18+
import { useModel } from '../generic/model-store';
1519
import SectionSubHeader from '../generic/section-sub-header';
16-
import { STATEFUL_BUTTON_STATES } from '../constants';
17-
import {
18-
getGradingSettings,
19-
getCourseAssignmentLists,
20-
getSavingStatus,
21-
getLoadingStatus,
22-
getCourseSettings,
23-
} from './data/selectors';
24-
import { fetchGradingSettings, sendGradingSetting, fetchCourseSettingsQuery } from './data/thunks';
25-
import GradingScale from './grading-scale/GradingScale';
26-
import GradingSidebar from './grading-sidebar';
27-
import messages from './messages';
20+
import SubHeader from '../generic/sub-header/SubHeader';
21+
import getPageHeadTitle from '../generic/utils';
2822
import AssignmentSection from './assignment-section';
2923
import CreditSection from './credit-section';
3024
import DeadlineSection from './deadline-section';
25+
import GradingScale from './grading-scale/GradingScale';
26+
import GradingSidebar from './grading-sidebar';
3127
import { useConvertGradeCutoffs, useUpdateGradingData } from './hooks';
32-
import getPageHeadTitle from '../generic/utils';
28+
import messages from './messages';
29+
30+
const GradingSettings = ({ courseId }) => {
31+
const intl = useIntl();
32+
const {
33+
data: gradingSettings,
34+
isLoading: isGradingSettingsLoading,
35+
} = useGradingSettings(courseId);
36+
const {
37+
data: courseSettingsData,
38+
isLoading: isCourseSettingsLoading,
39+
} = useCourseSettings(courseId);
40+
const {
41+
mutate: updateGradingSettings,
42+
isLoading: savePending,
43+
isSuccess: savingStatus,
44+
isError: savingFailed,
45+
} = useGradingSettingUpdater(courseId);
46+
47+
const courseAssignmentLists = gradingSettings?.courseAssignmentLists;
48+
const courseGradingDetails = gradingSettings?.courseDetails;
3349

34-
const GradingSettings = ({ intl, courseId }) => {
35-
const gradingSettingsData = useSelector(getGradingSettings);
36-
const courseSettingsData = useSelector(getCourseSettings);
37-
const courseAssignmentLists = useSelector(getCourseAssignmentLists);
38-
const savingStatus = useSelector(getSavingStatus);
39-
const loadingStatus = useSelector(getLoadingStatus);
4050
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
41-
const dispatch = useDispatch();
42-
const isLoading = loadingStatus === RequestStatus.IN_PROGRESS;
51+
const isLoading = isCourseSettingsLoading || isGradingSettingsLoading;
4352
const [isQueryPending, setIsQueryPending] = useState(false);
4453
const [showOverrideInternetConnectionAlert, setOverrideInternetConnectionAlert] = useState(false);
4554
const [eligibleGrade, setEligibleGrade] = useState(null);
4655

47-
const courseDetails = useModel('courseDetails', courseId);
48-
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
56+
const courseName = useModel('courseDetails', courseId)?.name;
4957

5058
const {
5159
graders,
@@ -60,7 +68,7 @@ const GradingSettings = ({ intl, courseId }) => {
6068
handleResetPageData,
6169
handleAddAssignment,
6270
handleRemoveAssignment,
63-
} = useUpdateGradingData(gradingSettingsData, setOverrideInternetConnectionAlert, setShowSuccessAlert);
71+
} = useUpdateGradingData(courseGradingDetails, setOverrideInternetConnectionAlert, setShowSuccessAlert);
6472

6573
const {
6674
gradeLetters,
@@ -69,28 +77,22 @@ const GradingSettings = ({ intl, courseId }) => {
6977
} = useConvertGradeCutoffs(gradeCutoffs);
7078

7179
useEffect(() => {
72-
if (savingStatus === RequestStatus.SUCCESSFUL) {
80+
if (savingStatus) {
7381
setShowSuccessAlert(!showSuccessAlert);
7482
setShowSavePrompt(!showSavePrompt);
7583
setTimeout(() => setShowSuccessAlert(false), 15000);
7684
setIsQueryPending(!isQueryPending);
7785
window.scrollTo({ top: 0, behavior: 'smooth' });
7886
}
79-
}, [savingStatus]);
80-
81-
useEffect(() => {
82-
dispatch(fetchGradingSettings(courseId));
83-
dispatch(fetchCourseSettingsQuery(courseId));
84-
}, [courseId]);
87+
}, [savePending]);
8588

8689
if (isLoading) {
87-
// eslint-disable-next-line react/jsx-no-useless-fragment
88-
return <></>;
90+
return null;
8991
}
9092

9193
const handleQueryProcessing = () => {
9294
setShowSuccessAlert(false);
93-
dispatch(sendGradingSetting(courseId, gradingData));
95+
updateGradingSettings(gradingData);
9496
};
9597

9698
const handleSendGradingSettingsData = () => {
@@ -110,11 +112,14 @@ const GradingSettings = ({ intl, courseId }) => {
110112
default: intl.formatMessage(messages.buttonSaveText),
111113
pending: intl.formatMessage(messages.buttonSavingText),
112114
},
113-
disabledStates: [RequestStatus.PENDING],
115+
disabledStates: [STATEFUL_BUTTON_STATES.pending],
114116
};
115117

116118
return (
117119
<>
120+
<Helmet>
121+
<title>{getPageHeadTitle(courseName, intl.formatMessage(messages.headingTitle))}</title>
122+
</Helmet>
118123
<Container size="xl" className="grading px-4">
119124
<div className="mt-5">
120125
<AlertMessage
@@ -156,6 +161,7 @@ const GradingSettings = ({ intl, courseId }) => {
156161
resetDataRef={resetDataRef}
157162
setOverrideInternetConnectionAlert={setOverrideInternetConnectionAlert}
158163
setEligibleGrade={setEligibleGrade}
164+
defaultGradeDesignations={gradingSettings?.defaultGradeDesignations}
159165
/>
160166
</section>
161167
{courseSettingsData.creditEligibilityEnabled && courseSettingsData.isCreditCourse && (
@@ -226,7 +232,7 @@ const GradingSettings = ({ intl, courseId }) => {
226232
<div className="alert-toast">
227233
{showOverrideInternetConnectionAlert && (
228234
<InternetConnectionAlert
229-
isFailed={savingStatus === RequestStatus.FAILED}
235+
isFailed={savingFailed}
230236
isQueryPending={isQueryPending}
231237
onQueryProcessing={handleQueryProcessing}
232238
onInternetConnectionFailed={handleInternetConnectionFailed}
@@ -263,8 +269,7 @@ const GradingSettings = ({ intl, courseId }) => {
263269
};
264270

265271
GradingSettings.propTypes = {
266-
intl: intlShape.isRequired,
267272
courseId: PropTypes.string.isRequired,
268273
};
269274

270-
export default injectIntl(GradingSettings);
275+
export default GradingSettings;
Lines changed: 70 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,32 @@
1-
import React from 'react';
2-
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
3-
import { AppProvider } from '@edx/frontend-platform/react';
41
import { initializeMockApp } from '@edx/frontend-platform';
52
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
6-
import { render, waitFor, fireEvent } from '@testing-library/react';
3+
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
4+
import { AppProvider } from '@edx/frontend-platform/react';
5+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
6+
import {
7+
act, fireEvent, render, screen,
8+
} from '@testing-library/react';
79
import MockAdapter from 'axios-mock-adapter';
10+
import React from 'react';
811

912
import initializeStore from '../store';
10-
import { getGradingSettingsApiUrl } from './data/api';
1113
import gradingSettings from './__mocks__/gradingSettings';
14+
import { getCourseSettingsApiUrl, getGradingSettingsApiUrl } from './data/api';
1215
import GradingSettings from './GradingSettings';
1316
import messages from './messages';
1417

1518
const courseId = '123';
1619
let axiosMock;
1720
let store;
1821

22+
const queryClient = new QueryClient();
23+
1924
const RootWrapper = () => (
2025
<AppProvider store={store}>
2126
<IntlProvider locale="en" messages={{}}>
22-
<GradingSettings intl={injectIntl} courseId={courseId} />
27+
<QueryClientProvider client={queryClient}>
28+
<GradingSettings intl={injectIntl} courseId={courseId} />
29+
</QueryClientProvider>
2330
</IntlProvider>
2431
</AppProvider>
2532
);
@@ -28,10 +35,7 @@ describe('<GradingSettings />', () => {
2835
beforeEach(() => {
2936
initializeMockApp({
3037
authenticatedUser: {
31-
userId: 3,
32-
username: 'abc123',
33-
administrator: true,
34-
roles: [],
38+
userId: 3, username: 'abc123', administrator: true, roles: [],
3539
},
3640
});
3741

@@ -40,52 +44,72 @@ describe('<GradingSettings />', () => {
4044
axiosMock
4145
.onGet(getGradingSettingsApiUrl(courseId))
4246
.reply(200, gradingSettings);
47+
axiosMock
48+
.onPost(getGradingSettingsApiUrl(courseId))
49+
.reply(200, {});
50+
axiosMock.onGet(getCourseSettingsApiUrl(courseId))
51+
.reply(200, {});
52+
render(<RootWrapper />);
4353
});
4454

45-
it('should render without errors', async () => {
46-
const { getByText, getAllByText } = render(<RootWrapper />);
47-
await waitFor(() => {
48-
const gradingElements = getAllByText(messages.headingTitle.defaultMessage);
49-
const gradingTitle = gradingElements[0];
50-
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
51-
expect(gradingTitle).toBeInTheDocument();
52-
expect(getByText(messages.policy.defaultMessage)).toBeInTheDocument();
53-
expect(getByText(messages.policiesDescription.defaultMessage)).toBeInTheDocument();
55+
function testSaving() {
56+
const saveBtn = screen.getByText(messages.buttonSaveText.defaultMessage);
57+
expect(saveBtn).toBeInTheDocument();
58+
fireEvent.click(saveBtn);
59+
expect(screen.getByText(messages.buttonSavingText.defaultMessage)).toBeInTheDocument();
60+
}
61+
62+
function setOnlineStatus(isOnline) {
63+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValue(isOnline);
64+
act(() => {
65+
window.dispatchEvent(new window.Event(isOnline ? 'online' : 'offline'));
5466
});
67+
}
68+
69+
it('should render without errors', async () => {
70+
const gradingElements = await screen.findAllByText(messages.headingTitle.defaultMessage);
71+
const gradingTitle = gradingElements[0];
72+
expect(screen.getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
73+
expect(gradingTitle).toBeInTheDocument();
74+
expect(screen.getByText(messages.policy.defaultMessage)).toBeInTheDocument();
75+
expect(screen.getByText(messages.policiesDescription.defaultMessage)).toBeInTheDocument();
5576
});
5677

5778
it('should update segment input value and show save alert', async () => {
58-
const { getByTestId, getAllByTestId } = render(<RootWrapper />);
59-
await waitFor(() => {
60-
const segmentInputs = getAllByTestId('grading-scale-segment-input');
61-
expect(segmentInputs).toHaveLength(5);
62-
const segmentInput = segmentInputs[1];
63-
fireEvent.change(segmentInput, { target: { value: 'Test' } });
64-
expect(segmentInput).toHaveValue('Test');
65-
expect(getByTestId('grading-settings-save-alert')).toBeVisible();
66-
});
79+
const segmentInputs = await screen.findAllByTestId('grading-scale-segment-input');
80+
expect(segmentInputs).toHaveLength(5);
81+
const segmentInput = segmentInputs[1];
82+
fireEvent.change(segmentInput, { target: { value: 'Test' } });
83+
expect(segmentInput).toHaveValue('Test');
84+
expect(screen.getByTestId('grading-settings-save-alert')).toBeVisible();
6785
});
6886

6987
it('should update grading scale segment input value on change and cancel the action', async () => {
70-
const { getByText, getAllByTestId } = render(<RootWrapper />);
71-
await waitFor(() => {
72-
const segmentInputs = getAllByTestId('grading-scale-segment-input');
73-
const segmentInput = segmentInputs[1];
74-
fireEvent.change(segmentInput, { target: { value: 'Test' } });
75-
fireEvent.click(getByText(messages.buttonCancelText.defaultMessage));
76-
expect(segmentInput).toHaveValue('a');
77-
});
88+
const segmentInputs = await screen.findAllByTestId('grading-scale-segment-input');
89+
const segmentInput = segmentInputs[1];
90+
fireEvent.change(segmentInput, { target: { value: 'Test' } });
91+
fireEvent.click(screen.getByText(messages.buttonCancelText.defaultMessage));
92+
expect(segmentInput).toHaveValue('a');
7893
});
94+
7995
it('should save segment input changes and display saving message', async () => {
80-
const { getByText, getAllByTestId } = render(<RootWrapper />);
81-
await waitFor(() => {
82-
const segmentInputs = getAllByTestId('grading-scale-segment-input');
83-
const segmentInput = segmentInputs[1];
84-
fireEvent.change(segmentInput, { target: { value: 'Test' } });
85-
const saveBtn = getByText(messages.buttonSaveText.defaultMessage);
86-
expect(saveBtn).toBeInTheDocument();
87-
fireEvent.click(saveBtn);
88-
expect(getByText(messages.buttonSavingText.defaultMessage)).toBeInTheDocument();
89-
});
96+
const segmentInputs = await screen.findAllByTestId('grading-scale-segment-input');
97+
const segmentInput = segmentInputs[1];
98+
fireEvent.change(segmentInput, { target: { value: 'Test' } });
99+
testSaving();
100+
});
101+
102+
it('should handle being offline gracefully', async () => {
103+
setOnlineStatus(false);
104+
const segmentInputs = await screen.findAllByTestId('grading-scale-segment-input');
105+
const segmentInput = segmentInputs[1];
106+
fireEvent.change(segmentInput, { target: { value: 'Test' } });
107+
const saveBtn = screen.getByText(messages.buttonSaveText.defaultMessage);
108+
expect(saveBtn).toBeInTheDocument();
109+
fireEvent.click(saveBtn);
110+
expect(screen.getByText(/studio's having trouble saving your work/i)).toBeInTheDocument();
111+
expect(screen.queryByText(messages.buttonSavingText.defaultMessage)).not.toBeInTheDocument();
112+
setOnlineStatus(true);
113+
testSaving();
90114
});
91115
});

src/grading-settings/data/apiHooks.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2+
import { getCourseSettings, getGradingSettings, sendGradingSettings } from './api';
3+
4+
export const useGradingSettings = (courseId) => (
5+
useQuery({
6+
queryKey: ['gradingSettings', courseId],
7+
queryFn: () => getGradingSettings(courseId),
8+
})
9+
);
10+
11+
export const useCourseSettings = (courseId) => (
12+
useQuery({
13+
queryKey: ['courseSettings', courseId],
14+
queryFn: () => getCourseSettings(courseId),
15+
})
16+
);
17+
18+
export const useGradingSettingUpdater = (courseId) => {
19+
const queryClient = useQueryClient();
20+
return useMutation({
21+
mutationFn: (settings) => sendGradingSettings(courseId, settings),
22+
onSettled: () => {
23+
queryClient.invalidateQueries({ queryKey: ['gradingSettings', courseId] });
24+
},
25+
});
26+
};

src/grading-settings/data/selectors.js

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)