@@ -88,14 +71,11 @@ export class GradebookTable extends React.Component {
}
GradebookTable.defaultProps = {
- areGradesFrozen: false,
grades: [],
};
GradebookTable.propTypes = {
// redux
- areGradesFrozen: PropTypes.bool,
- format: PropTypes.string.isRequired,
grades: PropTypes.arrayOf(PropTypes.shape({
percent: PropTypes.number,
section_breakdown: PropTypes.arrayOf(PropTypes.shape({
@@ -115,8 +95,6 @@ GradebookTable.propTypes = {
};
export const mapStateToProps = (state) => ({
- areGradesFrozen: selectors.assignmentTypes.areGradesFrozen(state),
- format: selectors.grades.gradeFormat(state),
grades: selectors.grades.allGrades(state),
headings: selectors.root.getHeadings(state),
});
diff --git a/src/components/GradesTab/GradebookTable/test.jsx b/src/components/GradesTab/GradebookTable/test.jsx
new file mode 100644
index 0000000..95a41ec
--- /dev/null
+++ b/src/components/GradesTab/GradebookTable/test.jsx
@@ -0,0 +1,162 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { Table } from '@edx/paragon';
+
+import selectors from 'data/selectors';
+import { Headings } from 'data/constants/grades';
+import LabelReplacements from './LabelReplacements';
+import Fields from './Fields';
+import { GradebookTable, mapStateToProps } from '.';
+
+jest.mock('@edx/paragon', () => ({
+ Table: () => 'Table',
+}));
+jest.mock('./Fields', () => ({
+ __esModule: true,
+ default: {
+ Username: () => 'Fields.Username',
+ Email: () => 'Fields.Email',
+ },
+}));
+jest.mock('./LabelReplacements', () => ({
+ __esModule: true,
+ default: {
+ TotalGradeLabelReplacement: () => 'TotalGradeLabelReplacement',
+ UsernameLabelReplacement: () => 'UsernameLabelReplacement',
+ },
+}));
+jest.mock('./GradeButton', () => 'GradeButton');
+jest.mock('data/selectors', () => ({
+ __esModule: true,
+ default: {
+ grades: {
+ roundGrade: jest.fn(grade => `roundedGrade: ${grade}`),
+ allGrades: jest.fn(state => ({ allGrades: state })),
+ },
+ root: {
+ getHeadings: jest.fn(state => ({ getHeadings: state })),
+ },
+ },
+}));
+describe('GradebookTable', () => {
+ describe('component', () => {
+ let el;
+ const fields = { field1: 'field1', field2: 'field2' };
+ const props = {
+ grades: [
+ {
+ percent: 1,
+ section_breakdown: [
+ { label: fields.field1, percent: 1.2 },
+ { label: fields.field2, percent: 2.3 },
+ ],
+ },
+ {
+ percent: 2,
+ section_breakdown: [
+ { label: fields.field1, percent: 1.2 },
+ { label: fields.field2, percent: 2.3 },
+ ],
+ },
+ {
+ percent: 3,
+ section_breakdown: [
+ { label: fields.field1, percent: 1.2 },
+ { label: fields.field2, percent: 2.3 },
+ ],
+ },
+ ],
+ headings: [
+ Headings.username,
+ Headings.email,
+ fields.field1,
+ fields.field2,
+ Headings.totalGrade,
+ ],
+ };
+ test('snapshot - fields1 and 2 between email and totalGrade, mocked rows', () => {
+ el = shallow(
);
+ el.instance().mapRows = (entry) => `mappedRow: ${entry.percent}`;
+ expect(el.instance().render()).toMatchSnapshot();
+ });
+ describe('table columns (mapHeaders)', () => {
+ let headings;
+ beforeEach(() => {
+ el = shallow(
);
+ headings = el.find(Table).props().columns;
+ });
+ test('username sets key and replaces label with component', () => {
+ const heading = headings[0];
+ expect(heading.key).toEqual(Headings.username);
+ expect(heading.label.type).toEqual(LabelReplacements.UsernameLabelReplacement);
+ });
+ test('email sets key and label from header', () => {
+ const heading = headings[1];
+ expect(heading.key).toEqual(Headings.email);
+ expect(heading.label).toEqual(Headings.email);
+ });
+ test('subsections set key and label from header', () => {
+ expect(headings[2]).toEqual({ key: fields.field1, label: fields.field1 });
+ expect(headings[3]).toEqual({ key: fields.field2, label: fields.field2 });
+ });
+ test('totalGrade sets key and replaces label with component', () => {
+ const heading = headings[4];
+ expect(heading.key).toEqual(Headings.totalGrade);
+ expect(heading.label.type).toEqual(LabelReplacements.TotalGradeLabelReplacement);
+ });
+ });
+ describe('table data (mapRows)', () => {
+ let rows;
+ beforeEach(() => {
+ el = shallow(
);
+ rows = el.find(Table).props().data;
+ });
+ describe.each([0, 1, 2])('gradeEntry($percent)', (gradeIndex) => {
+ let row;
+ const entry = props.grades[gradeIndex];
+ beforeEach(() => {
+ row = rows[gradeIndex];
+ });
+ test('username set to Username Field', () => {
+ const field = row[Headings.username];
+ expect(field.type).toEqual(Fields.Username);
+ expect(field.props).toEqual({
+ username: entry.username,
+ userKey: entry.external_user_key,
+ });
+ });
+ test('email set to Email Field', () => {
+ const field = row[Headings.email];
+ expect(field.type).toEqual(Fields.Email);
+ expect(field.props).toEqual({ email: entry.email });
+ });
+ test('totalGrade set to rounded percent grade * 100', () => {
+ expect(
+ row[Headings.totalGrade],
+ ).toEqual(`${selectors.grades.roundGrade(entry.percent * 100)}%`);
+ });
+ test('subsections loaded as GradeButtons', () => {
+ });
+ });
+ });
+ });
+ describe('mapStateToProps', () => {
+ let mapped;
+ const testState = {
+ where: 'did',
+ all: 'of',
+ these: 'bananas',
+ come: 'from?',
+ };
+ beforeEach(() => {
+ mapped = mapStateToProps(testState);
+ });
+ test('grades from grades.allGrades', () => {
+ expect(mapped.grades).toEqual(selectors.grades.allGrades(testState));
+ });
+ test('headings from root.getHeadings', () => {
+ expect(mapped.headings).toEqual(selectors.root.getHeadings(testState));
+ });
+ });
+});
diff --git a/src/data/constants/grades.js b/src/data/constants/grades.js
index 1eb0680..5425308 100644
--- a/src/data/constants/grades.js
+++ b/src/data/constants/grades.js
@@ -1,5 +1,24 @@
+import { StrictDict } from 'utils';
+
const EMAIL_HEADING = 'Email';
const TOTAL_COURSE_GRADE_HEADING = 'Total Grade (%)';
const USERNAME_HEADING = 'Username';
-export { EMAIL_HEADING, TOTAL_COURSE_GRADE_HEADING, USERNAME_HEADING };
+const GradeFormats = StrictDict({
+ absolute: 'absolute',
+ percent: 'percent',
+});
+
+const Headings = StrictDict({
+ email: 'Email',
+ totalGrade: 'Total Grade (%)',
+ username: 'Username',
+});
+
+export {
+ EMAIL_HEADING,
+ TOTAL_COURSE_GRADE_HEADING,
+ USERNAME_HEADING,
+ GradeFormats,
+ Headings,
+};
diff --git a/src/data/selectors/grades.js b/src/data/selectors/grades.js
index 7cceae7..63a3209 100644
--- a/src/data/selectors/grades.js
+++ b/src/data/selectors/grades.js
@@ -1,8 +1,9 @@
/* eslint-disable import/no-self-import */
import { StrictDict } from 'utils';
-import { formatDateForDisplay } from '../actions/utils';
+
+import { Headings, GradeFormats } from 'data/constants/grades';
+import { formatDateForDisplay } from 'data/actions/utils';
import simpleSelectorFactory from '../utils';
-import { EMAIL_HEADING, TOTAL_COURSE_GRADE_HEADING, USERNAME_HEADING } from '../constants/grades';
import * as module from './grades';
export const getRowsProcessed = ({
@@ -96,18 +97,14 @@ export const headingMapper = (category, label = 'All') => {
} else {
filter = filters.byLabel;
}
+ const { username, email, totalGrade } = Headings;
+ const fillerLabels = (entry) => entry.filter(filter).map(s => s.label);
- return (entry) => {
- if (entry) {
- return [
- USERNAME_HEADING,
- EMAIL_HEADING,
- ...entry.filter(filter).map(s => s.label),
- TOTAL_COURSE_GRADE_HEADING,
- ];
- }
- return [];
- };
+ return (entry) => (
+ entry
+ ? [username, email, ...fillerLabels(entry), totalGrade]
+ : []
+ );
};
/**
@@ -129,6 +126,37 @@ export const transformHistoryEntry = ({
...rest,
});
+/**
+ * roundGrade(val)
+ * Takes a number and rounds it to 2 decimal places
+ * defaults to 0
+ * @param {number=0} val - value to round.
+ * @return {number} - rounded number
+ */
+export const roundGrade = val => parseFloat((val || 0).toFixed(2));
+export const subsectionGrade = StrictDict({
+ /**
+ * subsectionGrade.absolute(subsection)
+ * returns rounded {earned}/{possible} if attempted, else ${earned}
+ * @param {object} subsection - grade subsection entry
+ * @return {string} - absolute-formatted subsection grade string
+ */
+ [GradeFormats.absolute]: (subsection) => {
+ const earned = module.roundGrade(subsection.score_earned);
+ const possible = module.roundGrade(subsection.score_possible);
+ return subsection.attempted ? `${earned}/${possible}` : `${earned}`;
+ },
+ /**
+ * subsectionGrade.percent(subsection)
+ * returns rounded percent times 100
+ * @param {object} subsection - grade subsection entry
+ * @return {string} - percent-formatted subsection grade string
+ */
+ [GradeFormats.percent]: (subsection) => (
+ module.roundGrade(subsection.percent * 100)
+ ),
+});
+
// Selectors
/**
* allGrades(state)
@@ -236,6 +264,9 @@ export default StrictDict({
hasOverrideErrors,
headingMapper,
+ roundGrade,
+ subsectionGrade,
+
...simpleSelectors,
allGrades,
bulkManagementHistoryEntries,
diff --git a/src/data/selectors/grades.test.js b/src/data/selectors/grades.test.js
index c857901..a372031 100644
--- a/src/data/selectors/grades.test.js
+++ b/src/data/selectors/grades.test.js
@@ -170,6 +170,50 @@ describe('grades selectors', () => {
});
});
+ describe('roundGrade', () => {
+ it('rounds values to 2 places', () => {
+ expect(selectors.roundGrade(23.124)).toEqual(23.12);
+ });
+ it('defaults to 0 if no value is passed', () => {
+ expect(selectors.roundGrade()).toEqual(0);
+ });
+ });
+
+ describe('subsectionGrade', () => {
+ const { roundGrade } = selectors;
+ beforeEach(() => {
+ selectors.roundGrade = jest.fn(grade => ({ roundGrade: grade }));
+ });
+ afterEach(() => {
+ selectors.roundGrade = roundGrade;
+ });
+ describe('absolute', () => {
+ const subsection = { score_earned: 2, score_possible: 5 };
+ describe('attempted', () => {
+ it('returns rounded {earned}/{possible}', () => {
+ const earned = selectors.roundGrade(subsection.score_earned);
+ const possible = selectors.roundGrade(subsection.score_possible);
+ expect(
+ selectors.subsectionGrade.absolute({ ...subsection, attempted: true }),
+ ).toEqual(`${earned}/${possible}`);
+ });
+ });
+ describe('not attempted', () => {
+ it('returns rounded {earned}', () => {
+ const earned = selectors.roundGrade(subsection.score_earned);
+ expect(selectors.subsectionGrade.absolute(subsection)).toEqual(`${earned}`);
+ });
+ });
+ });
+ describe('percent', () => {
+ it('returns rounded grade.percent * 100', () => {
+ const percent = 42;
+ const expected = selectors.roundGrade(percent * 100);
+ expect(selectors.subsectionGrade.percent({ percent })).toEqual(expected);
+ });
+ });
+ });
+
// Selectors
describe('allGrades', () => {
it('returns the grades results from redux state', () => {