feat: add user id parameter to progress page (#505)

This commit is contained in:
Matthew Piatetsky
2021-07-08 12:17:43 -04:00
committed by GitHub
parent e7c0ebdfe3
commit d2573a16b1
10 changed files with 108 additions and 24 deletions

View File

@@ -6,6 +6,7 @@ Object {
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
"courseStatus": "loaded",
"gradesFeatureIsLocked": false,
"targetUserId": undefined,
"toastBodyLink": null,
"toastBodyText": null,
"toastHeader": "",
@@ -302,6 +303,7 @@ Object {
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
"courseStatus": "loaded",
"gradesFeatureIsLocked": false,
"targetUserId": undefined,
"toastBodyLink": null,
"toastBodyText": null,
"toastHeader": "",
@@ -479,6 +481,7 @@ Object {
"courseId": "course-v1:edX+DemoX+Demo_Course_1",
"courseStatus": "loaded",
"gradesFeatureIsLocked": false,
"targetUserId": undefined,
"toastBodyLink": null,
"toastBodyText": null,
"toastHeader": "",

View File

@@ -211,8 +211,15 @@ export async function getDatesTabData(courseId) {
}
}
export async function getProgressTabData(courseId) {
const url = `${getConfig().LMS_BASE_URL}/api/course_home/v1/progress/${courseId}`;
export async function getProgressTabData(courseId, targetUserId) {
let url = `${getConfig().LMS_BASE_URL}/api/course_home/v1/progress/${courseId}`;
// If targetUserId is passed in, we will get the progress page data
// for the user with the provided id, rather than the requesting user.
if (targetUserId) {
url += `/${targetUserId}/`;
}
try {
const { data } = await getAuthenticatedHttpClient().get(url);
const camelCasedData = camelCaseObject(data);

View File

@@ -115,6 +115,20 @@ describe('Data layer integration tests', () => {
expect(state.courseHome.courseStatus).toEqual('loaded');
expect(state).toMatchSnapshot();
});
it('Should handle the url including a targetUserId', async () => {
const progressTabData = Factory.build('progressTabData', { courseId });
const targetUserId = 2;
const progressUrl = `${progressBaseUrl}/${courseId}/${targetUserId}/`;
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata);
axiosMock.onGet(progressUrl).reply(200, progressTabData);
await executeThunk(thunks.fetchProgressTab(courseId, 2), store.dispatch);
const state = store.getState();
expect(state.courseHome.targetUserId).toEqual(2);
});
});
describe('Test saveCourseGoal', () => {

View File

@@ -22,6 +22,7 @@ const slice = createSlice({
},
fetchTabSuccess: (state, { payload }) => {
state.courseId = payload.courseId;
state.targetUserId = payload.targetUserId;
state.courseStatus = LOADED;
},
fetchTabFailure: (state, { payload }) => {

View File

@@ -27,12 +27,12 @@ const eventTypes = {
POST_EVENT: 'post_event',
};
export function fetchTab(courseId, tab, getTabData) {
export function fetchTab(courseId, tab, getTabData, targetUserId) {
return async (dispatch) => {
dispatch(fetchTabRequest({ courseId }));
Promise.allSettled([
getCourseHomeCourseMetadata(courseId),
getTabData(courseId),
getTabData(courseId, targetUserId),
]).then(([courseHomeCourseMetadataResult, tabDataResult]) => {
const fetchedCourseHomeCourseMetadata = courseHomeCourseMetadataResult.status === 'fulfilled';
const fetchedTabData = tabDataResult.status === 'fulfilled';
@@ -62,7 +62,7 @@ export function fetchTab(courseId, tab, getTabData) {
}
if (fetchedCourseHomeCourseMetadata && fetchedTabData) {
dispatch(fetchTabSuccess({ courseId }));
dispatch(fetchTabSuccess({ courseId, targetUserId }));
} else {
dispatch(fetchTabFailure({ courseId }));
}
@@ -74,8 +74,8 @@ export function fetchDatesTab(courseId) {
return fetchTab(courseId, 'dates', getDatesTabData);
}
export function fetchProgressTab(courseId) {
return fetchTab(courseId, 'progress', getProgressTabData);
export function fetchProgressTab(courseId, targetUserId) {
return fetchTab(courseId, 'progress', getProgressTabData, parseInt(targetUserId, 10) || targetUserId);
}
export function fetchOutlineTab(courseId) {

View File

@@ -12,16 +12,23 @@ import messages from './messages';
function ProgressHeader({ intl }) {
const {
courseId,
targetUserId,
} = useSelector(state => state.courseHome);
const { administrator } = getAuthenticatedUser();
const { administrator, userId } = getAuthenticatedUser();
const { studioUrl } = useModel('progress', courseId);
const { studioUrl, username } = useModel('progress', courseId);
const viewingOtherStudentsProgressPage = (targetUserId && targetUserId !== userId);
const pageTitle = viewingOtherStudentsProgressPage
? intl.formatMessage(messages.progressHeaderForTargetUser, { username })
: intl.formatMessage(messages.progressHeader);
return (
<>
<div className="row w-100 m-0 mt-3 mb-4 justify-content-between">
<h1>{intl.formatMessage(messages.progressHeader)}</h1>
<h1>{pageTitle}</h1>
{administrator && studioUrl && (
<Button variant="outline-primary" size="sm" className="align-self-center" href={studioUrl}>
{intl.formatMessage(messages.studioLink)}

View File

@@ -22,7 +22,7 @@ describe('Progress Tab', () => {
const courseId = 'course-v1:edX+Test+run';
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/course_metadata/${courseId}`;
courseMetadataUrl = appendBrowserTimezoneToUrl(courseMetadataUrl);
const progressUrl = `${getConfig().LMS_BASE_URL}/api/course_home/v1/progress/${courseId}`;
const progressUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/course_home/v1/progress/*`);
const store = initializeStore();
const defaultMetadata = Factory.build('courseHomeMetadata', { id: courseId });
@@ -1034,4 +1034,16 @@ describe('Progress Tab', () => {
expect(screen.queryByTestId('certificate-status-component')).not.toBeInTheDocument();
});
});
describe('Viewing progress page of other students by changing url', () => {
it('Changing the url changes the header', async () => {
setMetadata({ is_enrolled: true });
setTabData({ username: 'otherstudent' });
await executeThunk(thunks.fetchProgressTab(courseId, 10), store.dispatch);
await act(async () => render(<ProgressTab />, { store }));
expect(screen.getByText('Course progress for otherstudent')).toBeInTheDocument();
});
});
});

View File

@@ -5,6 +5,11 @@ const messages = defineMessages({
id: 'progress.header',
defaultMessage: 'Your progress',
},
progressHeaderForTargetUser: {
id: 'progress.header.targetUser',
defaultMessage: 'Course progress for {username}',
description: 'Header when displaying the progress for a different user',
},
studioLink: {
id: 'progress.link.studio',
defaultMessage: 'View grading in Studio',

View File

@@ -44,11 +44,21 @@ subscribe(APP_READY, () => {
<DatesTab />
</TabContainer>
</PageRoute>
<PageRoute path="/course/:courseId/progress">
<TabContainer tab="progress" fetch={fetchProgressTab} slice="courseHome">
<ProgressTab />
</TabContainer>
</PageRoute>
<PageRoute
path={[
'/course/:courseId/progress/:targetUserId/',
'/course/:courseId/progress',
]}
render={({ match }) => (
<TabContainer
tab="progress"
fetch={(courseId) => fetchProgressTab(courseId, match.params.targetUserId)}
slice="courseHome"
>
<ProgressTab />
</TabContainer>
)}
/>
<PageRoute path="/course/:courseId/course-end">
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
<CourseExit />

View File

@@ -5,7 +5,7 @@ import { initializeTestStore, render, screen } from '../setupTest';
import { TabContainer } from './index';
const mockDispatch = jest.fn();
const mockFetch = jest.fn().mockImplementation((x) => x);
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch,
@@ -13,15 +13,18 @@ jest.mock('react-redux', () => ({
jest.mock('./TabPage', () => () => <div data-testid="TabPage" />);
describe('Tab Container', () => {
const mockData = {
children: [],
fetch: mockFetch,
tab: 'dummy',
slice: 'courseware',
};
let courseId;
let mockFetch;
let mockData;
beforeAll(async () => {
beforeEach(async () => {
mockFetch = jest.fn().mockImplementation((x) => x);
mockData = {
children: [],
fetch: mockFetch,
tab: 'dummy',
slice: 'courseware',
};
const store = await initializeTestStore({ excludeFetchSequence: true });
courseId = store.getState().courseware.courseId;
});
@@ -42,4 +45,26 @@ describe('Tab Container', () => {
.toHaveBeenCalledWith(courseId);
expect(screen.getByTestId('TabPage')).toBeInTheDocument();
});
it('Should handle passing in a targetUserId', () => {
const targetUserId = '1';
history.push(`/course/${courseId}/progress/${targetUserId}/`);
render(
<Route
path="/course/:courseId/progress/:targetUserId/"
render={({ match }) => (
<TabContainer
fetch={() => mockFetch(match.params.courseId, match.params.targetUserId)}
slice="courseHome"
/>
)}
/>,
);
expect(mockFetch)
.toHaveBeenCalledTimes(1)
.toHaveBeenCalledWith(courseId, targetUserId);
expect(screen.getByTestId('TabPage')).toBeInTheDocument();
});
});