@@ -222,9 +176,7 @@ const StudentsFilters = ({ resetPagination, fetchData, setFilters }) => {
};
StudentsFilters.propTypes = {
- fetchData: PropTypes.func.isRequired,
resetPagination: PropTypes.func.isRequired,
- setFilters: PropTypes.func.isRequired,
};
export default StudentsFilters;
diff --git a/src/features/Students/StudentsFilters/reducer.jsx b/src/features/Students/StudentsFilters/reducer.jsx
deleted file mode 100644
index 362db041..00000000
--- a/src/features/Students/StudentsFilters/reducer.jsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import {
- FETCH_COURSES_DATA_FAILURE,
- FETCH_COURSES_DATA_REQUEST,
- FETCH_COURSES_DATA_SUCCESS,
- FETCH_CLASSES_DATA_REQUEST,
- FETCH_CLASSES_DATA_SUCCESS,
- FETCH_CLASSES_DATA_FAILURE,
-} from 'features/Students/actionTypes';
-import { RequestStatus } from 'features/constants';
-
-const reducer = (state, action) => {
- switch (action.type) {
- case FETCH_COURSES_DATA_REQUEST:
- return {
- ...state,
- courses: {
- ...state.courses,
- status: RequestStatus.LOADING,
- },
- };
- case FETCH_COURSES_DATA_SUCCESS:
- return {
- ...state,
- courses: {
- ...state.courses,
- status: RequestStatus.SUCCESS,
- data: action.payload,
- },
- };
- case FETCH_COURSES_DATA_FAILURE:
- return {
- ...state,
- courses: {
- ...state.courses,
- status: RequestStatus.ERROR,
- error: action.payload,
- },
- };
- case FETCH_CLASSES_DATA_REQUEST:
- return {
- ...state,
- classes: {
- ...state.classes,
- status: RequestStatus.LOADING,
- },
- };
- case FETCH_CLASSES_DATA_SUCCESS:
- return {
- ...state,
- classes: {
- ...state.classes,
- status: RequestStatus.SUCCESS,
- data: action.payload,
- },
- };
- case FETCH_CLASSES_DATA_FAILURE:
- return {
- ...state,
- classes: {
- ...state.classes,
- status: RequestStatus.ERROR,
- error: action.payload,
- },
- };
- default:
- return state;
- }
-};
-
-export default reducer;
diff --git a/src/features/Students/StudentsMetrics/_test_/index.test.jsx b/src/features/Students/StudentsMetrics/_test_/index.test.jsx
index 44e66678..c772e7a3 100644
--- a/src/features/Students/StudentsMetrics/_test_/index.test.jsx
+++ b/src/features/Students/StudentsMetrics/_test_/index.test.jsx
@@ -1,12 +1,22 @@
import React from 'react';
+import { Provider } from 'react-redux';
import { render } from '@testing-library/react';
import StudentsMetrics from 'features/Students/StudentsMetrics';
import '@testing-library/jest-dom/extend-expect';
+import { initializeStore } from 'store';
+
+let store;
describe('StudentsMetrics component', () => {
+ beforeEach(() => {
+ store = initializeStore();
+ });
+
test('renders components', () => {
const { getByText } = render(
-
,
+
+
+ ,
);
expect(getByText('This week')).toBeInTheDocument();
diff --git a/src/features/Students/StudentsMetrics/_test_/reducer.test.jsx b/src/features/Students/StudentsMetrics/_test_/reducer.test.jsx
deleted file mode 100644
index 08bb8de4..00000000
--- a/src/features/Students/StudentsMetrics/_test_/reducer.test.jsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import {
- FETCH_METRICS_DATA_REQUEST,
- FETCH_METRICS_DATA_SUCCESS,
- FETCH_METRICS_DATA_FAILURE,
-} from 'features/Students/actionTypes';
-import { RequestStatus } from 'features/constants';
-import reducer from 'features/Students/StudentsMetrics/reducer';
-
-describe('Student filter reducers', () => {
- const initialState = {
- data: [],
- status: RequestStatus.SUCCESS,
- error: null,
- };
-
- test('should handle FETCH_METRICS_DATA_REQUEST', () => {
- const state = {
- ...initialState,
- status: RequestStatus.LOADING,
- };
- const action = {
- type: FETCH_METRICS_DATA_REQUEST,
- };
- expect(reducer(state, action)).toEqual(state);
- });
-
- test('should handle FETCH_METRICS_DATA_SUCCESS', () => {
- const state = {
- ...initialState,
- status: RequestStatus.SUCCESS,
- data: [],
- };
- const action = {
- type: FETCH_METRICS_DATA_SUCCESS,
- payload: [],
- };
- expect(reducer(state, action)).toEqual(state);
- });
-
- test('should handle FETCH_METRICS_DATA_FAILURE', () => {
- const state = {
- ...initialState,
- status: RequestStatus.ERROR,
- error: '',
- };
- const action = {
- type: FETCH_METRICS_DATA_FAILURE,
- payload: '',
- };
- expect(reducer(state, action)).toEqual(state);
- });
-});
diff --git a/src/features/Students/StudentsMetrics/index.jsx b/src/features/Students/StudentsMetrics/index.jsx
index a45e75e1..5fde3a94 100644
--- a/src/features/Students/StudentsMetrics/index.jsx
+++ b/src/features/Students/StudentsMetrics/index.jsx
@@ -1,42 +1,18 @@
-import React, { useEffect, useReducer } from 'react';
+import React, { useEffect } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
import { Card, CardGrid, ToggleButton } from '@edx/paragon';
import { ToggleButtonGroup } from 'react-paragon-topaz';
-import { logError } from '@edx/frontend-platform/logging';
-import { camelCaseObject } from '@edx/frontend-platform';
-import { getMetricsStudents } from 'features/Students/data/api';
-import { RequestStatus } from 'features/constants';
-import reducer from 'features/Students/StudentsMetrics/reducer';
-import {
- FETCH_METRICS_DATA_REQUEST,
- FETCH_METRICS_DATA_SUCCESS,
- FETCH_METRICS_DATA_FAILURE,
-} from 'features/Students/actionTypes';
-import 'features/Students/StudentsMetrics/index.scss';
+import { fetchMetricsData } from 'features/Students/data/thunks';
-const initialState = {
- data: [],
- status: RequestStatus.SUCCESS,
- error: null,
-};
+import 'features/Students/StudentsMetrics/index.scss';
const StudentsMetrics = () => {
- const [state, dispatch] = useReducer(reducer, initialState);
-
- const fetchMetricsData = async () => {
- dispatch({ type: FETCH_METRICS_DATA_REQUEST });
-
- try {
- const response = camelCaseObject(await getMetricsStudents());
- dispatch({ type: FETCH_METRICS_DATA_SUCCESS, payload: response });
- } catch (error) {
- dispatch({ type: FETCH_METRICS_DATA_FAILURE, payload: error });
- logError(error);
- }
- };
+ const dispatch = useDispatch();
+ const stateMetrics = useSelector((state) => state.students.metrics.data);
useEffect(() => {
- fetchMetricsData();
- }, []);
+ dispatch(fetchMetricsData());
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
@@ -63,7 +39,7 @@ const StudentsMetrics = () => {
title="New students registered"
/>
- {state.data.newStudentsRegistered}
+ {stateMetrics.newStudentsRegistered}
@@ -71,7 +47,7 @@ const StudentsMetrics = () => {
title="Classes scheduled"
/>
- {state.data.classesScheduled}
+ {stateMetrics.classesScheduled}
diff --git a/src/features/Students/StudentsMetrics/reducer.jsx b/src/features/Students/StudentsMetrics/reducer.jsx
deleted file mode 100644
index 0e839e41..00000000
--- a/src/features/Students/StudentsMetrics/reducer.jsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import {
- FETCH_METRICS_DATA_REQUEST,
- FETCH_METRICS_DATA_SUCCESS,
- FETCH_METRICS_DATA_FAILURE,
-} from 'features/Students/actionTypes';
-import { RequestStatus } from 'features/constants';
-
-const reducer = (state, action) => {
- switch (action.type) {
- case FETCH_METRICS_DATA_REQUEST:
- return {
- ...state,
- status: RequestStatus.LOADING,
- };
- case FETCH_METRICS_DATA_SUCCESS:
- return {
- ...state,
- status: RequestStatus.SUCCESS,
- data: action.payload,
- };
- case FETCH_METRICS_DATA_FAILURE:
- return {
- ...state,
- status: RequestStatus.ERROR,
- error: action.payload,
- };
- default:
- return state;
- }
-};
-
-export default reducer;
diff --git a/src/features/Students/StudentsPage/_test_/index.test.jsx b/src/features/Students/StudentsPage/_test_/index.test.jsx
index 9b9cca0c..1f163363 100644
--- a/src/features/Students/StudentsPage/_test_/index.test.jsx
+++ b/src/features/Students/StudentsPage/_test_/index.test.jsx
@@ -1,10 +1,7 @@
import React from 'react';
import axios from 'axios';
import StudentsPage from 'features/Students/StudentsPage';
-import {
- render,
- waitFor,
-} from '@testing-library/react';
+import { render, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { Provider } from 'react-redux';
import { initializeStore } from 'store';
diff --git a/src/features/Students/StudentsPage/_test_/reducer.test.jsx b/src/features/Students/StudentsPage/_test_/reducer.test.jsx
deleted file mode 100644
index fc99db58..00000000
--- a/src/features/Students/StudentsPage/_test_/reducer.test.jsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import {
- FETCH_STUDENTS_DATA_REQUEST,
- FETCH_STUDENTS_DATA_SUCCESS,
- FETCH_STUDENTS_DATA_FAILURE,
- UPDATE_CURRENT_PAGE,
-} from 'features/Students/actionTypes';
-import { RequestStatus } from 'features/constants';
-import reducer from 'features/Students/StudentsPage/reducer';
-
-describe('Instructor page reducers', () => {
- const initialState = {
- data: [],
- error: null,
- currentPage: 1,
- numPages: 0,
- };
-
- test('should handle FETCH_STUDENTS_DATA_REQUEST', () => {
- const state = {
- ...initialState,
- status: RequestStatus.LOADING,
- };
- const action = {
- type: FETCH_STUDENTS_DATA_REQUEST,
- };
- expect(reducer(state, action)).toEqual(state);
- });
-
- test('should handle FETCH_STUDENTS_DATA_SUCCESS', () => {
- const state = {
- ...initialState,
- status: RequestStatus.SUCCESS,
- count: 0,
- };
- const action = {
- type: FETCH_STUDENTS_DATA_SUCCESS,
- payload: {
- results: [],
- count: 0,
- numPages: 0,
- },
- };
- expect(reducer(state, action)).toEqual(state);
- });
-
- test('should handle FETCH_STUDENTS_DATA_FAILURE', () => {
- const state = {
- ...initialState,
- status: RequestStatus.ERROR,
- error: '',
- };
- const action = {
- type: FETCH_STUDENTS_DATA_FAILURE,
- payload: '',
- };
- expect(reducer(state, action)).toEqual(state);
- });
-
- test('should handle UPDATE_CURRENT_PAGE', () => {
- const state = {
- ...initialState,
- currentPage: 1,
- };
- const action = {
- type: UPDATE_CURRENT_PAGE,
- payload: 1,
- };
- expect(reducer(state, action)).toEqual(state);
- });
-});
diff --git a/src/features/Students/StudentsPage/index.jsx b/src/features/Students/StudentsPage/index.jsx
index 37561f7f..ed2c958e 100644
--- a/src/features/Students/StudentsPage/index.jsx
+++ b/src/features/Students/StudentsPage/index.jsx
@@ -1,61 +1,30 @@
-import { getStudentbyInstitutionAdmin } from 'features/Students/data/api';
+import React, { useEffect, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
import { StudentsTable } from 'features/Students/StudentsTable/index';
import StudentsFilters from 'features/Students/StudentsFilters';
import StudentsMetrics from 'features/Students/StudentsMetrics';
-import { RequestStatus } from 'features/constants';
-import reducer from 'features/Students/StudentsPage/reducer';
-
-import { logError } from '@edx/frontend-platform/logging';
import Container from '@edx/paragon/dist/Container';
-
-import React, { useEffect, useState, useReducer } from 'react';
-import { camelCaseObject } from '@edx/frontend-platform';
-import {
- Pagination,
-} from '@edx/paragon';
-import {
- FETCH_STUDENTS_DATA_REQUEST,
- FETCH_STUDENTS_DATA_SUCCESS,
- FETCH_STUDENTS_DATA_FAILURE,
- UPDATE_CURRENT_PAGE,
-} from 'features/Students/actionTypes';
-
-const initialState = {
- data: [],
- status: RequestStatus.SUCCESS,
- error: null,
- currentPage: 1,
- numPages: 0,
-};
+import { Pagination } from '@edx/paragon';
+import { updateCurrentPage } from 'features/Students/data/slice';
+import { fetchStudentsData } from 'features/Students/data/thunks';
+import { initialPage } from 'features/constants';
const StudentsPage = () => {
- const [state, dispatch] = useReducer(reducer, initialState);
- const [currentPage, setCurrentPage] = useState(1);
- const [filters, setFilters] = useState({});
-
- const fetchData = async (filtersData) => {
- dispatch({ type: FETCH_STUDENTS_DATA_REQUEST });
-
- try {
- const response = camelCaseObject(await getStudentbyInstitutionAdmin(currentPage, filtersData));
- dispatch({ type: FETCH_STUDENTS_DATA_SUCCESS, payload: response.data });
- } catch (error) {
- dispatch({ type: FETCH_STUDENTS_DATA_FAILURE, payload: error });
- logError(error);
- }
- };
+ const dispatch = useDispatch();
+ const stateStudents = useSelector((state) => state.students);
+ const [currentPage, setCurrentPage] = useState(initialPage);
useEffect(() => {
- fetchData(filters); // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [currentPage, filters]);
+ dispatch(fetchStudentsData(currentPage, stateStudents.filters));
+ }, [currentPage]); // eslint-disable-line react-hooks/exhaustive-deps
const resetPagination = () => {
- setCurrentPage(1);
+ setCurrentPage(initialPage);
};
const handlePagination = (targetPage) => {
setCurrentPage(targetPage);
- dispatch({ type: UPDATE_CURRENT_PAGE, payload: targetPage });
+ dispatch(updateCurrentPage(targetPage));
};
return (
@@ -63,16 +32,15 @@ const StudentsPage = () => {
Students
-
+
- {state.numPages > 1 && (
+ {stateStudents.table.numPages > 1 && (
{
- switch (action.type) {
- case FETCH_STUDENTS_DATA_REQUEST:
- return { ...state, status: RequestStatus.LOADING };
- case FETCH_STUDENTS_DATA_SUCCESS: {
- const { results, count, numPages } = action.payload;
- return {
- ...state,
- status: RequestStatus.SUCCESS,
- data: results,
- numPages,
- count,
- };
- }
- case FETCH_STUDENTS_DATA_FAILURE:
- return {
- ...state,
- status: RequestStatus.ERROR,
- error: action.payload,
- };
- case UPDATE_CURRENT_PAGE:
- return {
- ...state,
- currentPage: action.payload,
- };
- default:
- return state;
- }
-};
-
-export default reducer;
diff --git a/src/features/Students/StudentsTable/_test_/index.test.jsx b/src/features/Students/StudentsTable/_test_/index.test.jsx
index a6bde8ab..01f1a917 100644
--- a/src/features/Students/StudentsTable/_test_/index.test.jsx
+++ b/src/features/Students/StudentsTable/_test_/index.test.jsx
@@ -1,12 +1,24 @@
import React from 'react';
+import { Provider } from 'react-redux';
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import { StudentsTable } from 'features/Students/StudentsTable';
import { getColumns } from 'features/Students/StudentsTable/columns';
+import { initializeStore } from 'store';
+
+let store;
describe('Student Table', () => {
+ beforeEach(() => {
+ store = initializeStore();
+ });
+
test('renders StudentsTable without data', () => {
- render();
+ render(
+
+
+ ,
+ );
const emptyTableText = screen.getByText('No students found.');
expect(emptyTableText).toBeInTheDocument();
});
@@ -44,34 +56,29 @@ describe('Student Table', () => {
];
const component = render(
- ,
+
+
+ ,
);
- // Check if the table rows are present
const tableRows = screen.getAllByRole('row');
- expect(tableRows).toHaveLength(data.length + 1); // Data rows + 1 header row
+ expect(tableRows).toHaveLength(data.length + 1);
- // Check student names
expect(component.container).toHaveTextContent('Student 1');
expect(component.container).toHaveTextContent('Student 2');
- // Check course names
expect(component.container).toHaveTextContent('course 1');
expect(component.container).toHaveTextContent('course 2');
- // Check class names
expect(component.container).toHaveTextContent('class 1');
expect(component.container).toHaveTextContent('class 2');
- // Check instructors names
expect(component.container).toHaveTextContent('Instructor 1');
expect(component.container).toHaveTextContent('Instructor 2');
- // Check exam ready
expect(component.container).toHaveTextContent('yes');
expect(component.container).toHaveTextContent('no');
- // Ensure "No students found." is not present
expect(screen.queryByText('No students found.')).toBeNull();
});
});
diff --git a/src/features/Students/StudentsTable/index.jsx b/src/features/Students/StudentsTable/index.jsx
index 2b605e23..e9827c16 100644
--- a/src/features/Students/StudentsTable/index.jsx
+++ b/src/features/Students/StudentsTable/index.jsx
@@ -1,4 +1,5 @@
import React, { useState, useMemo } from 'react';
+import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import DataTable from '@edx/paragon/dist/DataTable';
import {
@@ -12,12 +13,13 @@ import {
import { IntlProvider } from 'react-intl';
import { handleEnrollments } from 'features/Students/data/api';
import { getColumns, hideColumns } from 'features/Students/StudentsTable/columns';
+import { fetchStudentsData } from 'features/Students/data/thunks';
const StudentsTable = ({
data,
count,
- fetchData,
}) => {
+ const dispatch = useDispatch();
const [isOpenAlertModal, openAlertModal, closeAlertModal] = useToggle(false);
const [selectedRow, setRow] = useState({});
const COLUMNS = useMemo(() => getColumns({ openAlertModal, setRow }), [openAlertModal]);
@@ -27,7 +29,7 @@ const StudentsTable = ({
const handleStudentsActions = async () => {
await handleEnrollments(enrollmentData, selectedRow.ccxId);
- fetchData();
+ dispatch(fetchStudentsData());
closeAlertModal();
};
@@ -72,13 +74,11 @@ const StudentsTable = ({
StudentsTable.propTypes = {
data: PropTypes.arrayOf(PropTypes.shape([])),
count: PropTypes.number,
- fetchData: PropTypes.func,
};
StudentsTable.defaultProps = {
data: [],
count: 0,
- fetchData: null,
};
export { StudentsTable };
diff --git a/src/features/Students/actionTypes.js b/src/features/Students/actionTypes.js
deleted file mode 100644
index 7f025608..00000000
--- a/src/features/Students/actionTypes.js
+++ /dev/null
@@ -1,17 +0,0 @@
-export const FETCH_STUDENTS_DATA_REQUEST = 'FETCH_STUDENTS_DATA_REQUEST';
-export const FETCH_STUDENTS_DATA_SUCCESS = 'FETCH_STUDENTS_DATA_SUCCESS';
-export const FETCH_STUDENTS_DATA_FAILURE = 'FETCH_STUDENTS_DATA_FAILURE';
-
-export const FETCH_COURSES_DATA_REQUEST = 'FETCH_COURSES_DATA_REQUEST';
-export const FETCH_COURSES_DATA_SUCCESS = 'FETCH_COURSES_DATA_SUCCESS';
-export const FETCH_COURSES_DATA_FAILURE = 'FETCH_COURSES_DATA_FAILURE';
-
-export const FETCH_CLASSES_DATA_REQUEST = 'FETCH_CLASSES_DATA_REQUEST';
-export const FETCH_CLASSES_DATA_SUCCESS = 'FETCH_CLASSES_DATA_SUCCESS';
-export const FETCH_CLASSES_DATA_FAILURE = 'FETCH_CLASSES_DATA_FAILURE';
-
-export const FETCH_METRICS_DATA_REQUEST = 'FETCH_METRICS_DATA_REQUEST';
-export const FETCH_METRICS_DATA_SUCCESS = 'FETCH_METRICS_DATA_SUCCESS';
-export const FETCH_METRICS_DATA_FAILURE = 'FETCH_METRICS_DATA_FAILURE';
-
-export const UPDATE_CURRENT_PAGE = 'UPDATE_CURRENT_PAGE';
diff --git a/src/features/Students/data/_test_/redux.test.js b/src/features/Students/data/_test_/redux.test.js
new file mode 100644
index 00000000..37a7db6f
--- /dev/null
+++ b/src/features/Students/data/_test_/redux.test.js
@@ -0,0 +1,228 @@
+import MockAdapter from 'axios-mock-adapter';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { initializeMockApp } from '@edx/frontend-platform/testing';
+import {
+ fetchStudentsData,
+ fetchCoursesData,
+ fetchClassesData,
+ fetchMetricsData,
+} from 'features/Students/data/thunks';
+import { updateCurrentPage, updateFilters } from 'features/Students/data/slice';
+import { executeThunk } from 'test-utils';
+import { initializeStore } from 'store';
+
+let axiosMock;
+let store;
+
+describe('Students redux tests', () => {
+ beforeEach(() => {
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 1,
+ username: 'testuser',
+ administrator: true,
+ roles: [],
+ },
+ });
+ axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+
+ store = initializeStore();
+ });
+
+ afterEach(() => {
+ axiosMock.reset();
+ });
+
+ test('successful fetch students data', async () => {
+ const studentsApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/students/`;
+ const mockResponse = {
+ results: [
+ {
+ learnerName: 'pending_enrollment',
+ learnerEmail: 'student04@example.com',
+ instructors: [
+ 'Instructor01',
+ ],
+ courseName: 'Demo Course 1',
+ classId: 'ccx-v1:demo+demo1+2020+ccx@2',
+ className: 'test ccx1',
+ status: 'Pending',
+ examReady: false,
+ },
+ ],
+ count: 2,
+ num_pages: 1,
+ current_page: 1,
+ };
+ axiosMock.onGet(studentsApiUrl)
+ .reply(200, mockResponse);
+
+ expect(store.getState().students.table.status)
+ .toEqual('loading');
+
+ await executeThunk(fetchStudentsData(), store.dispatch, store.getState);
+
+ expect(store.getState().students.table.data)
+ .toEqual(mockResponse.results);
+
+ expect(store.getState().students.table.status)
+ .toEqual('success');
+ });
+
+ test('failed fetch students data', async () => {
+ const studentsApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/students/`;
+ axiosMock.onGet(studentsApiUrl)
+ .reply(500);
+
+ expect(store.getState().students.table.status)
+ .toEqual('loading');
+
+ await executeThunk(fetchStudentsData(), store.dispatch, store.getState);
+
+ expect(store.getState().students.table.data)
+ .toEqual([]);
+
+ expect(store.getState().students.table.status)
+ .toEqual('error');
+ });
+
+ test('successful fetch courses data', async () => {
+ const studentsApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/courses/?limit=false&institution_id=1`;
+ const mockResponse = [
+ {
+ masterCourseName: 'Demo Course 1',
+ numberOfClasses: 1,
+ missingClassesForInstructor: null,
+ numberOfStudents: 1,
+ numberOfPendingStudents: 11,
+ },
+ ];
+ axiosMock.onGet(studentsApiUrl)
+ .reply(200, mockResponse);
+
+ expect(store.getState().students.courses.status)
+ .toEqual('loading');
+
+ await executeThunk(fetchCoursesData(1), store.dispatch, store.getState);
+
+ expect(store.getState().students.courses.data)
+ .toEqual(mockResponse);
+
+ expect(store.getState().students.courses.status)
+ .toEqual('success');
+ });
+
+ test('failed fetch courses data', async () => {
+ const studentsApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/courses/?limit=false&institution_id=1`;
+ axiosMock.onGet(studentsApiUrl)
+ .reply(500);
+
+ expect(store.getState().students.courses.status)
+ .toEqual('loading');
+
+ await executeThunk(fetchCoursesData(1), store.dispatch, store.getState);
+
+ expect(store.getState().students.courses.data)
+ .toEqual([]);
+
+ expect(store.getState().students.courses.status)
+ .toEqual('error');
+ });
+
+ test('successful fetch classes data', async () => {
+ const studentsApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/?limit=false&institution_id=1&course_name=Demo`;
+ const mockResponse = [
+ {
+ classId: 'ccx-v1:demo+demo1+2020+ccx@2',
+ className: 'test ccx1',
+ masterCourseName: 'Demo',
+ },
+ ];
+ axiosMock.onGet(studentsApiUrl)
+ .reply(200, mockResponse);
+
+ expect(store.getState().students.classes.status)
+ .toEqual('loading');
+
+ await executeThunk(fetchClassesData(1, 'Demo'), store.dispatch, store.getState);
+
+ expect(store.getState().students.classes.data)
+ .toEqual(mockResponse);
+
+ expect(store.getState().students.classes.status)
+ .toEqual('success');
+ });
+
+ test('failed fetch classes data', async () => {
+ const studentsApiUrl = `${process.env.COURSE_OPERATIONS_API_V2_BASE_URL}/classes/?limit=false&institution_id=1`;
+ axiosMock.onGet(studentsApiUrl)
+ .reply(500);
+
+ expect(store.getState().students.classes.status)
+ .toEqual('loading');
+
+ await executeThunk(fetchClassesData(1), store.dispatch, store.getState);
+
+ expect(store.getState().students.classes.data)
+ .toEqual([]);
+
+ expect(store.getState().students.classes.status)
+ .toEqual('error');
+ });
+
+ test('successful fetch metrics data', async () => {
+ const mockResponse = {
+ classesScheduled: '71%',
+ newStudentsRegistered: '367',
+ };
+
+ expect(store.getState().students.metrics.status)
+ .toEqual('loading');
+
+ await executeThunk(fetchMetricsData(), store.dispatch, store.getState);
+
+ expect(store.getState().students.metrics.data)
+ .toEqual(mockResponse);
+
+ expect(store.getState().students.metrics.status)
+ .toEqual('success');
+ });
+
+ test('update current page', () => {
+ const newPage = 2;
+ const intialState = store.getState().students.table;
+ const expectState = {
+ ...intialState,
+ currentPage: newPage,
+ };
+
+ store.dispatch(updateCurrentPage(newPage));
+ expect(store.getState().students.table).toEqual(expectState);
+ });
+
+ test('update current page', () => {
+ const newPage = 2;
+ const intialState = store.getState().students.table;
+ const expectState = {
+ ...intialState,
+ currentPage: newPage,
+ };
+
+ store.dispatch(updateCurrentPage(newPage));
+ expect(store.getState().students.table).toEqual(expectState);
+ });
+
+ test('update filters', () => {
+ const filters = {
+ course_name: 'Demo Course 1',
+ };
+ const intialState = store.getState().students.filters;
+ const expectState = {
+ ...intialState,
+ ...filters,
+ };
+
+ store.dispatch(updateFilters(filters));
+ expect(store.getState().students.filters).toEqual(expectState);
+ });
+});
diff --git a/src/features/Students/data/api.js b/src/features/Students/data/api.js
index 940fc8e8..2985b06c 100644
--- a/src/features/Students/data/api.js
+++ b/src/features/Students/data/api.js
@@ -34,8 +34,10 @@ function getClassesByInstitution(institutionId, courseName) {
function getMetricsStudents() {
const metricsData = {
- new_students_registered: '367',
- classes_scheduled: '71%',
+ data: {
+ new_students_registered: '367',
+ classes_scheduled: '71%',
+ },
};
return metricsData;
}
diff --git a/src/features/Students/data/index.js b/src/features/Students/data/index.js
new file mode 100644
index 00000000..49aea945
--- /dev/null
+++ b/src/features/Students/data/index.js
@@ -0,0 +1,2 @@
+export { reducer } from 'features/Students/data/slice';
+export { fetchStudentsData } from 'features/Students/data/thunks';
diff --git a/src/features/Students/data/slice.js b/src/features/Students/data/slice.js
new file mode 100644
index 00000000..4234404d
--- /dev/null
+++ b/src/features/Students/data/slice.js
@@ -0,0 +1,99 @@
+/* eslint-disable no-param-reassign */
+import { createSlice } from '@reduxjs/toolkit';
+import { RequestStatus, initialStateService } from 'features/constants';
+
+const initialState = {
+ table: {
+ currentPage: 1,
+ data: [],
+ status: RequestStatus.LOADING,
+ error: null,
+ numPages: 0,
+ count: 0,
+ },
+ courses: {
+ ...initialStateService,
+ },
+ classes: {
+ ...initialStateService,
+ },
+ metrics: {
+ ...initialStateService,
+ },
+ filters: {},
+};
+
+export const studentsSlice = createSlice({
+ name: 'students',
+ initialState,
+ reducers: {
+ updateCurrentPage: (state, { payload }) => {
+ state.table.currentPage = payload;
+ },
+ fetchStudentsDataRequest: (state) => {
+ state.table.status = RequestStatus.LOADING;
+ },
+ fetchStudentsDataSuccess: (state, { payload }) => {
+ const { results, count, numPages } = payload;
+ state.table.status = RequestStatus.SUCCESS;
+ state.table.data = results;
+ state.table.numPages = numPages;
+ state.table.count = count;
+ },
+ fetchStudentsDataFailed: (state) => {
+ state.table.status = RequestStatus.ERROR;
+ },
+ updateFilters: (state, { payload }) => {
+ state.filters = payload;
+ },
+ fetchCoursesDataRequest: (state) => {
+ state.courses.status = RequestStatus.LOADING;
+ },
+ fetchCoursesDataSuccess: (state, { payload }) => {
+ state.courses.status = RequestStatus.SUCCESS;
+ state.courses.data = payload;
+ },
+ fetchCoursesDataFailed: (state) => {
+ state.courses.status = RequestStatus.ERROR;
+ },
+ fetchClassesDataRequest: (state) => {
+ state.classes.status = RequestStatus.LOADING;
+ },
+ fetchClassesDataSuccess: (state, { payload }) => {
+ state.classes.status = RequestStatus.SUCCESS;
+ state.classes.data = payload;
+ },
+ fetchClassesDataFailed: (state) => {
+ state.classes.status = RequestStatus.ERROR;
+ },
+ fetchMetricsDataRequest: (state) => {
+ state.metrics.status = RequestStatus.LOADING;
+ },
+ fetchMetricsDataSuccess: (state, { payload }) => {
+ state.metrics.status = RequestStatus.SUCCESS;
+ state.metrics.data = payload;
+ },
+ fetchMetricsDataFailed: (state) => {
+ state.metrics.status = RequestStatus.ERROR;
+ },
+ },
+});
+
+export const {
+ updateCurrentPage,
+ fetchStudentsDataRequest,
+ fetchStudentsDataSuccess,
+ fetchStudentsDataFailed,
+ updateFilters,
+ fetchCoursesDataRequest,
+ fetchCoursesDataSuccess,
+ fetchCoursesDataFailed,
+ fetchClassesDataRequest,
+ fetchClassesDataSuccess,
+ fetchClassesDataFailed,
+ fetchMetricsDataRequest,
+ fetchMetricsDataSuccess,
+ fetchMetricsDataFailed,
+} = studentsSlice.actions;
+
+export const { reducer } = studentsSlice;
diff --git a/src/features/Students/data/thunks.js b/src/features/Students/data/thunks.js
new file mode 100644
index 00000000..160579c5
--- /dev/null
+++ b/src/features/Students/data/thunks.js
@@ -0,0 +1,81 @@
+import { logError } from '@edx/frontend-platform/logging';
+import { camelCaseObject } from '@edx/frontend-platform';
+import {
+ fetchStudentsDataRequest,
+ fetchStudentsDataSuccess,
+ fetchStudentsDataFailed,
+ fetchCoursesDataRequest,
+ fetchCoursesDataSuccess,
+ fetchCoursesDataFailed,
+ fetchClassesDataRequest,
+ fetchClassesDataSuccess,
+ fetchClassesDataFailed,
+ fetchMetricsDataRequest,
+ fetchMetricsDataSuccess,
+ fetchMetricsDataFailed,
+} from 'features/Students/data/slice';
+import { getStudentbyInstitutionAdmin, getClassesByInstitution, getMetricsStudents } from 'features/Students/data/api';
+import { getCoursesByInstitution } from 'features/Common/data/api';
+
+function fetchStudentsData(currentPage, filtersData) {
+ return async (dispatch) => {
+ dispatch(fetchStudentsDataRequest());
+
+ try {
+ const response = camelCaseObject(await getStudentbyInstitutionAdmin(currentPage, filtersData));
+ dispatch(fetchStudentsDataSuccess(response.data));
+ } catch (error) {
+ dispatch(fetchStudentsDataFailed());
+ logError(error);
+ }
+ };
+}
+
+function fetchCoursesData(id) {
+ return async (dispatch) => {
+ dispatch(fetchCoursesDataRequest());
+
+ try {
+ const response = camelCaseObject(await getCoursesByInstitution(id, false));
+ dispatch(fetchCoursesDataSuccess(response.data));
+ } catch (error) {
+ dispatch(fetchCoursesDataFailed());
+ logError(error);
+ }
+ };
+}
+
+function fetchClassesData(id, courseName) {
+ return async (dispatch) => {
+ dispatch(fetchClassesDataRequest());
+
+ try {
+ const response = camelCaseObject(await getClassesByInstitution(id, courseName));
+ dispatch(fetchClassesDataSuccess(response.data));
+ } catch (error) {
+ dispatch(fetchClassesDataFailed());
+ logError(error);
+ }
+ };
+}
+
+function fetchMetricsData() {
+ return async (dispatch) => {
+ dispatch(fetchMetricsDataRequest());
+
+ try {
+ const response = camelCaseObject(await getMetricsStudents());
+ dispatch(fetchMetricsDataSuccess(response.data));
+ } catch (error) {
+ dispatch(fetchMetricsDataFailed());
+ logError(error);
+ }
+ };
+}
+
+export {
+ fetchStudentsData,
+ fetchCoursesData,
+ fetchClassesData,
+ fetchMetricsData,
+};
diff --git a/src/features/constants.js b/src/features/constants.js
index 6b12f26c..3bbead96 100644
--- a/src/features/constants.js
+++ b/src/features/constants.js
@@ -8,3 +8,20 @@ export const RequestStatus = {
SUCCESS: 'success',
ERROR: 'error',
};
+
+/**
+ * Obj for initial service state.
+ * @object
+ */
+export const initialStateService = {
+ data: [],
+ status: RequestStatus.LOADING,
+ error: null,
+};
+
+/**
+ * Number for initial page.
+ * @readonly
+ * @number
+ */
+export const initialPage = 1;
diff --git a/src/store.js b/src/store.js
index b01a3d08..b5d44a04 100644
--- a/src/store.js
+++ b/src/store.js
@@ -1,12 +1,16 @@
import { configureStore } from '@reduxjs/toolkit';
import { reducer as instructorsReducer } from 'features/Instructors/data';
import { reducer as mainReducer } from 'features/Main/data';
+import { reducer as coursesReducer } from 'features/Courses/data';
+import { reducer as studentsReducer } from 'features/Students/data';
export function initializeStore(preloadedState = undefined) {
return configureStore({
reducer: {
instructors: instructorsReducer,
main: mainReducer,
+ courses: coursesReducer,
+ students: studentsReducer,
},
preloadedState,
});