Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/openResponses/OpenResponsesPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Container } from '@openedx/paragon';
import ORSummary from './components/ORSummary';

const OpenResponsesPage = () => {
return (
<Container fluid>
<ORSummary />
</Container>
);
};

export default OpenResponsesPage;
82 changes: 82 additions & 0 deletions src/openResponses/components/ORSummary.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { screen } from '@testing-library/react';
import { useParams } from 'react-router-dom';
import ORSummary from './ORSummary';
import { useOpenResponsesData } from '../../data/apiHook';
import messages from '../messages';
import { renderWithIntl } from '../../testUtils';

jest.mock('react-router-dom', () => ({
useParams: jest.fn(),
}));

jest.mock('../../data/apiHook', () => ({
useOpenResponsesData: jest.fn(),
}));

const mockData = {
totalUnits: '5',
totalAssessments: '10',
totalResponses: '15',
training: '2',
peer: '3',
self: '4',
waiting: '1',
staff: '6',
finalGradeReceived: '7',
};

describe('ORSummary', () => {
beforeEach(() => {
(useParams as jest.Mock).mockReturnValue({ courseId: 'course-v1:edX+Test+2024' });
});

it('renders all summary titles', () => {
(useOpenResponsesData as jest.Mock).mockReturnValue({ data: {} });
renderWithIntl(<ORSummary />);
expect(screen.getByText(messages.summaryTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.totalUnits.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.totalAssessments.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.totalResponses.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.training.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.peer.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.self.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.waiting.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.staff.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.finalGradeReceived.defaultMessage)).toBeInTheDocument();
});

it('renders default values when data is empty', () => {
(useOpenResponsesData as jest.Mock).mockReturnValue({ data: {} });
renderWithIntl(<ORSummary />);
expect(screen.getAllByText('0').length).toBe(9);
});

it('renders values from data', () => {
(useOpenResponsesData as jest.Mock).mockReturnValue({
data: mockData,
});
renderWithIntl(<ORSummary />);
expect(screen.getByText('5')).toBeInTheDocument();
expect(screen.getByText('10')).toBeInTheDocument();
expect(screen.getByText('15')).toBeInTheDocument();
expect(screen.getByText('2')).toBeInTheDocument();
expect(screen.getByText('3')).toBeInTheDocument();
expect(screen.getByText('4')).toBeInTheDocument();
expect(screen.getByText('1')).toBeInTheDocument();
expect(screen.getByText('6')).toBeInTheDocument();
expect(screen.getByText('7')).toBeInTheDocument();
});

it('renders icons', () => {
(useOpenResponsesData as jest.Mock).mockReturnValue({ data: mockData });
renderWithIntl(<ORSummary />);
expect(screen.getAllByRole('img', { hidden: true }).length).toBe(2);
});

it('uses courseId from params', () => {
(useParams as jest.Mock).mockReturnValue({ courseId: 'course-v1:edX+Another+2024' });
(useOpenResponsesData as jest.Mock).mockReturnValue({ data: {} });
renderWithIntl(<ORSummary />);
expect(useOpenResponsesData).toHaveBeenCalledWith('course-v1:edX+Another+2024');
});
});
99 changes: 99 additions & 0 deletions src/openResponses/components/ORSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { useParams } from 'react-router-dom';
import { useIntl } from '@openedx/frontend-base';
import { Icon } from '@openedx/paragon';
import { EditNote, ViewDay } from '@openedx/paragon/icons';
import { useOpenResponsesData } from '../../data/apiHook';
import messages from '../messages';

const ORSummary = () => {
const intl = useIntl();
const { courseId = '' } = useParams<{ courseId: string }>();
const { data = {} } = useOpenResponsesData(courseId);
const {
totalUnits = '0',
totalAssessments = '0',
totalResponses = '0',
training = '0',
peer = '0',
self = '0',
waiting = '0',
staff = '0',
finalGradeReceived = '0',
} = data;

return (
<>
<h3>{intl.formatMessage(messages.summaryTitle)}</h3>
<div className="container-mw-xl">
<div className="row">
<div className="col-sm-3 col-md-2 col-xl-1">
<p className="mb-2">{intl.formatMessage(messages.totalUnits)}</p>
</div>
<div className="col-sm-4 col-md-3 col-xl-2">
<p className="mb-2">{intl.formatMessage(messages.totalAssessments)}</p>
</div>
</div>
<div className="row lead">
<div className="col-sm-3 col-md-2 col-xl-1 d-flex align-items-center">
<Icon src={ViewDay} />
<p className="ml-2 mb-0">{totalUnits}</p>
</div>
<div className="col-sm-4 col-md-3 col-xl-2 d-flex align-items-center">
<Icon src={EditNote} size="lg" />
<p className="ml-2 mb-0">{totalAssessments}</p>
</div>
</div>
</div>
<div className="container-mw-xl mt-3">
<div className="row align-items-end">
<div className="col-sm-2 col-lg-1">
<p className="mb-2">{intl.formatMessage(messages.totalResponses)}</p>
</div>
<div className="col-sm-2 col-lg-1">
<p className="mb-2">{intl.formatMessage(messages.training)}</p>
</div>
<div className="col-sm-1">
<p className="mb-2">{intl.formatMessage(messages.peer)}</p>
</div>
<div className="col-sm-1">
<p className="mb-2">{intl.formatMessage(messages.self)}</p>
</div>
<div className="col-sm-2 col-lg-1">
<p className="mb-2">{intl.formatMessage(messages.waiting)}</p>
</div>
<div className="col-sm-2 col-lg-1">
<p className="mb-2">{intl.formatMessage(messages.staff)}</p>
</div>
<div className="col-sm-2 col-lg-1">
<p className="mb-2">{intl.formatMessage(messages.finalGradeReceived)}</p>
</div>
</div>
<div className="row lead">
<div className="col-sm-2 col-lg-1">
<p>{totalResponses}</p>
</div>
<div className="col-sm-2 col-lg-1">
<p>{training}</p>
</div>
<div className="col-sm-1">
<p>{peer}</p>
</div>
<div className="col-sm-1">
<p>{self}</p>
</div>
<div className="col-sm-2 col-lg-1">
<p>{waiting}</p>
</div>
<div className="col-sm-2 col-lg-1">
<p>{staff}</p>
</div>
<div className="col-sm-2 col-lg-1">
<p>{finalGradeReceived}</p>
</div>
</div>
</div>
</>
);
};

export default ORSummary;
8 changes: 8 additions & 0 deletions src/openResponses/data/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getAuthenticatedHttpClient, camelCaseObject } from '@openedx/frontend-base';
import { getApiBaseUrl } from '../../data/api';

export const getOpenResponsesData = async (courseId: string) => {
const url = `${getApiBaseUrl()}/api/instructor/v2/courses/${courseId}/open-responses`;
const { data } = await getAuthenticatedHttpClient().get(url);
return camelCaseObject(data);
};
11 changes: 11 additions & 0 deletions src/openResponses/data/apiHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import { getOpenResponsesData } from './api';
import { openResponsesQueryKeys } from './queryKeys';

export const useOpenResponsesData = (courseId: string) => (
useQuery({
queryKey: openResponsesQueryKeys.byCourse(courseId),
queryFn: () => getOpenResponsesData(courseId),
enabled: !!courseId,
})
);
6 changes: 6 additions & 0 deletions src/openResponses/data/queryKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { appId } from '../../constants';

export const openResponsesQueryKeys = {
all: [appId, 'openResponses'],
byCourse: (courseId: string) => [...openResponsesQueryKeys.all, courseId],
};
56 changes: 56 additions & 0 deletions src/openResponses/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { defineMessages } from '@openedx/frontend-base';

const messages = defineMessages({
summaryTitle: {
id: 'instruct.openResponses.summary',
defaultMessage: 'Summary',
description: 'Title for the summary section in open responses'
},
totalUnits: {
id: 'instruct.openResponses.label.totalUnits',
defaultMessage: 'Total Units',
description: 'Label for the total units in open responses'
},
totalAssessments: {
id: 'instruct.openResponses.label.totalAssessments',
defaultMessage: 'Total Assessments',
description: 'Label for the total assessments in open responses'
},
totalResponses: {
id: 'instruct.openResponses.label.totalResponses',
defaultMessage: 'Total Responses',
description: 'Label for the total responses'
},
training: {
id: 'instruct.openResponses.label.training',
defaultMessage: 'Training',
description: 'Label for the training count'
},
peer: {
id: 'instruct.openResponses.label.peer',
defaultMessage: 'Peer',
description: 'Label for the peer count'
},
self: {
id: 'instruct.openResponses.label.self',
defaultMessage: 'Self',
description: 'Label for the self count'
},
waiting: {
id: 'instruct.openResponses.label.waiting',
defaultMessage: 'Waiting',
description: 'Label for the waiting count'
},
staff: {
id: 'instruct.openResponses.label.staff',
defaultMessage: 'Staff',
description: 'Label for the staff count'
},
finalGradeReceived: {
id: 'instruct.openResponses.label.finalGradeReceived',
defaultMessage: 'Final Grade Received',
description: 'Label for the final grade received count'
}
});

export default messages;
9 changes: 5 additions & 4 deletions src/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import CohortsPage from './cohorts/CohortsPage';
import CourseInfoPage from './courseInfo/CourseInfoPage';
import Main from './Main';
import OpenResponsesPage from './openResponses/OpenResponsesPage';

const routes = [
{
Expand Down Expand Up @@ -43,10 +44,10 @@ const routes = [
// path: 'certificates',
// element: <CertificatesPage />
// },
// {
// path: 'open_responses',
// element: <OpenResponsesPage />
// }
{
path: 'open_responses',
element: <OpenResponsesPage />
}
]
}
];
Expand Down