Compare commits

..

6 Commits

Author SHA1 Message Date
jansenk
32be24cfe6 feat: don't show masters-only columns for courses without a masters track 2023-05-08 15:05:00 -04:00
Peter Kulko
67789481fb chore: use openedx brand as a default theme (#327) 2023-05-04 21:22:46 +03:00
Jansen Kantor
543cd623e1 feat: add column for full name for masters students (#321)
* feat: add column for full name for masters students

* refactor: move masters asterisk out of messages file

* refactor: simpletext -> text

* refactor: asterisk const
2023-04-28 13:04:31 -04:00
dependabot[bot]
ba31b713e2 build(deps): bump webpack from 5.75.0 to 5.79.0
Bumps [webpack](https://github.com/webpack/webpack) from 5.75.0 to 5.79.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.75.0...v5.79.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-28 10:42:41 -04:00
jansenk
84fe2c6628 chore: update browserslist DB 2023-04-28 10:34:43 -04:00
Leangseu Kim
b87447b543 fix: file input handler 2023-04-26 15:32:02 -04:00
22 changed files with 26515 additions and 2489 deletions

View File

@@ -11,18 +11,22 @@ on:
jobs:
test:
runs-on: ubuntu-20.04
strategy:
matrix:
node: [16]
npm: [8.5.x]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
uses: actions/checkout@v2
- name: Setup Nodejs
uses: actions/setup-node@v3
uses: actions/setup-node@v1
with:
node-version: ${{ env.NODE_VER }}
node-version: ${{ matrix.node }}
- name: Install npm 8.5.x
run: npm install -g npm@${{ matrix.npm }}
- name: Install dependencies
run: npm ci

View File

@@ -10,4 +10,4 @@ on:
jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master

View File

@@ -15,13 +15,10 @@ jobs:
with:
fetch-depth: 0
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: ${{ env.NODE_VER }}
node-version: 12
- name: Install dependencies
run: npm ci

1
.nvmrc
View File

@@ -1 +0,0 @@
18.15

28714
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,7 @@
"extends @edx/browserslist-config"
],
"dependencies": {
"@edx/brand": "npm:@edx/brand-edx.org@^1.3.2",
"@edx/brand": "npm:@edx/brand-openedx@^1.2.0",
"@edx/frontend-component-footer": "^11.1.1",
"@edx/frontend-component-header": "^3.1.1",
"@edx/frontend-platform": "2.5.0",
@@ -68,7 +68,7 @@
},
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-build": "12.8.27",
"@edx/frontend-build": "^12.4.15",
"@testing-library/react": "^12.1.0",
"axios": "0.21.2",
"axios-mock-adapter": "^1.17.0",

View File

@@ -29,16 +29,16 @@ Username.propTypes = {
};
/**
* Fields.Email
* Simple label field for email value.
* @param {string} email - email for display
* Fields.Text
* Simple label field for text value.
* @param {string} value - value for display
*/
const Email = ({ email }) => <span className="wrap-text-in-cell">{email}</span>;
Email.propTypes = {
email: PropTypes.string.isRequired,
const Text = ({ value }) => (<span className="wrap-text-in-cell">{value}</span>);
Text.propTypes = {
value: PropTypes.string.isRequired,
};
export default StrictDict({
Email,
Text,
Username,
});

View File

@@ -41,13 +41,13 @@ describe('Gradebook Table Fields', () => {
});
});
describe('Email', () => {
const email = 'myTag@place.com';
describe('Text', () => {
const value = 'myTag@place.com';
test('snapshot', () => {
expect(shallow(<Fields.Email email={email} />)).toMatchSnapshot();
expect(shallow(<Fields.Text value={value} />)).toMatchSnapshot();
});
test('wraps entry email', () => {
expect(shallow(<Fields.Email email={email} />).text()).toEqual(email);
test('wraps entry value', () => {
expect(shallow(<Fields.Text value={value} />).text()).toEqual(value);
});
});
});

View File

@@ -45,6 +45,13 @@ const TotalGradeLabelReplacement = () => (
</div>
);
/**
* Asterisk to display next to heading labels that are only used for masters students
*/
const mastersOnlyFieldAsterisk = (
<span className="font-weight-normal">*</span>
);
/**
* <UsernameLabelReplacement />
* Username column header. Lists that Student Key is possibly available
@@ -56,11 +63,24 @@ const UsernameLabelReplacement = () => (
</div>
<div className="font-weight-normal student-key">
<FormattedMessage {...messages.studentKeyLabel} />
{ mastersOnlyFieldAsterisk }
</div>
</div>
);
/**
* <MastersOnlyLabelReplacement {message}>
* Column header for fields that are only available for masters students
*/
const MastersOnlyLabelReplacement = (message) => (
<div>
<FormattedMessage {...message} />
{ mastersOnlyFieldAsterisk }
</div>
);
export default StrictDict({
TotalGradeLabelReplacement,
UsernameLabelReplacement,
MastersOnlyLabelReplacement,
});

View File

@@ -9,6 +9,7 @@ import LabelReplacements from './LabelReplacements';
const {
TotalGradeLabelReplacement,
UsernameLabelReplacement,
MastersOnlyLabelReplacement,
} = LabelReplacements;
jest.mock('@edx/paragon', () => ({
@@ -35,6 +36,16 @@ describe('LabelReplacements', () => {
expect(shallow(<UsernameLabelReplacement />)).toMatchSnapshot();
});
});
describe('MastersOnlyLabelReplacement', () => {
test('snapshot', () => {
const message = {
id: 'id',
defaultMessage: 'defaultMessAge',
description: 'desCripTion',
};
expect(shallow(<MastersOnlyLabelReplacement {...message} />)).toMatchSnapshot();
});
});
});
describe('snapshot', () => {

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Gradebook Table Fields Email snapshot 1`] = `
exports[`Gradebook Table Fields Text snapshot 1`] = `
<span
className="wrap-text-in-cell"
>

View File

@@ -1,5 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LabelReplacements MastersOnlyLabelReplacement snapshot 1`] = `
<div>
<FormattedMessage
defaultMessage="defaultMessAge"
description="desCripTion"
id="id"
/>
<span
className="font-weight-normal"
>
*
</span>
</div>
`;
exports[`LabelReplacements TotalGradeLabelReplacement displays overlay tooltip 1`] = `
<Tooltip
id="course-grade-tooltip"
@@ -73,10 +88,15 @@ exports[`LabelReplacements UsernameLabelReplacement snapshot 1`] = `
className="font-weight-normal student-key"
>
<FormattedMessage
defaultMessage="Student Key*"
defaultMessage="Student Key"
description="Gradebook table Student Key label"
id="gradebook.GradesView.table.labels.studentKey"
/>
<span
className="font-weight-normal"
>
*
</span>
</div>
</div>
`;

View File

@@ -13,8 +13,16 @@ exports[`GradebookTable component snapshot - fields1 and 2 between email and tot
"accessor": "Username",
},
Object {
"Header": <FormattedMessage
defaultMessage="Email*"
"Header": <MastersOnlyLabelReplacement
defaultMessage="Full Name"
description="Gradebook table full name column header"
id="gradebook.GradesView.table.headings.fullName"
/>,
"accessor": "Full Name",
},
Object {
"Header": <MastersOnlyLabelReplacement
defaultMessage="Email"
description="Gradebook table email column header"
id="gradebook.GradesView.table.headings.email"
/>,

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { DataTable } from '@edx/paragon';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import { Headings } from 'data/constants/grades';
@@ -38,7 +38,9 @@ export class GradebookTable extends React.Component {
} else if (heading === Headings.username) {
label = <LabelReplacements.UsernameLabelReplacement />;
} else if (heading === Headings.email) {
label = <FormattedMessage {...messages.emailHeading} />;
label = <LabelReplacements.MastersOnlyLabelReplacement {...messages.emailHeading} />;
} else if (heading === Headings.fullName) {
label = <LabelReplacements.MastersOnlyLabelReplacement {...messages.fullNameHeading} />;
} else {
label = heading;
}
@@ -49,7 +51,8 @@ export class GradebookTable extends React.Component {
[Headings.username]: (
<Fields.Username username={entry.username} userKey={entry.external_user_key} />
),
[Headings.email]: (<Fields.Email email={entry.email} />),
[Headings.fullName]: (<Fields.Text value={entry.full_name} />),
[Headings.email]: (<Fields.Text value={entry.email} />),
[Headings.totalGrade]: `${roundGrade(entry.percent * 100)}${getLocalizedPercentSign()}`,
...entry.section_breakdown.reduce((acc, subsection) => ({
...acc,

View File

@@ -1,9 +1,14 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
fullNameHeading: {
id: 'gradebook.GradesView.table.headings.fullName',
defaultMessage: 'Full Name',
description: 'Gradebook table full name column header',
},
emailHeading: {
id: 'gradebook.GradesView.table.headings.email',
defaultMessage: 'Email*',
defaultMessage: 'Email',
description: 'Gradebook table email column header',
},
totalGradeHeading: {
@@ -18,7 +23,7 @@ const messages = defineMessages({
},
studentKeyLabel: {
id: 'gradebook.GradesView.table.labels.studentKey',
defaultMessage: 'Student Key*',
defaultMessage: 'Student Key',
description: 'Gradebook table Student Key label',
},
usernameLabel: {

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { shallow } from 'enzyme';
import { DataTable } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import { Headings } from 'data/constants/grades';
@@ -22,7 +21,7 @@ jest.mock('./Fields', () => ({
__esModule: true,
default: {
Username: () => 'Fields.Username',
Email: () => 'Fields.Email',
Text: () => 'Fields.Text',
},
}));
jest.mock('./LabelReplacements', () => ({
@@ -30,6 +29,7 @@ jest.mock('./LabelReplacements', () => ({
default: {
TotalGradeLabelReplacement: () => 'TotalGradeLabelReplacement',
UsernameLabelReplacement: () => 'UsernameLabelReplacement',
MastersOnlyLabelReplacement: () => 'MastersOnlyLabelReplacement',
},
}));
jest.mock('./GradeButton', () => 'GradeButton');
@@ -75,6 +75,7 @@ describe('GradebookTable', () => {
],
headings: [
Headings.username,
Headings.fullName,
Headings.email,
fields.field1,
fields.field2,
@@ -104,17 +105,22 @@ describe('GradebookTable', () => {
expect(heading.accessor).toEqual(Headings.username);
expect(heading.Header.type).toEqual(LabelReplacements.UsernameLabelReplacement);
});
test('email sets key and Header from header', () => {
test('full name sets key and Header from header', () => {
const heading = headings[1];
expect(heading.accessor).toEqual(Headings.fullName);
expect(heading.Header).toEqual(<LabelReplacements.MastersOnlyLabelReplacement {...messages.fullNameHeading} />);
});
test('email sets key and Header from header', () => {
const heading = headings[2];
expect(heading.accessor).toEqual(Headings.email);
expect(heading.Header).toEqual(<FormattedMessage {...messages.emailHeading} />);
expect(heading.Header).toEqual(<LabelReplacements.MastersOnlyLabelReplacement {...messages.emailHeading} />);
});
test('subsections set key and Header from header', () => {
expect(headings[2]).toEqual({ accessor: fields.field1, Header: fields.field1 });
expect(headings[3]).toEqual({ accessor: fields.field2, Header: fields.field2 });
expect(headings[3]).toEqual({ accessor: fields.field1, Header: fields.field1 });
expect(headings[4]).toEqual({ accessor: fields.field2, Header: fields.field2 });
});
test('totalGrade sets key and replaces Header with component', () => {
const heading = headings[4];
const heading = headings[5];
expect(heading.accessor).toEqual(Headings.totalGrade);
expect(heading.Header.type).toEqual(LabelReplacements.TotalGradeLabelReplacement);
});
@@ -139,10 +145,15 @@ describe('GradebookTable', () => {
userKey: entry.external_user_key,
});
});
test('email set to Email Field', () => {
test('fullName set to Text Field', () => {
const field = row[Headings.fullName];
expect(field.type).toEqual(Fields.Text);
expect(field.props).toEqual({ value: entry.full_name });
});
test('email set to Text Field', () => {
const field = row[Headings.email];
expect(field.type).toEqual(Fields.Email);
expect(field.props).toEqual({ email: entry.email });
expect(field.type).toEqual(Fields.Text);
expect(field.props).toEqual({ value: entry.email });
});
test('totalGrade set to rounded percent grade * 100', () => {
expect(

View File

@@ -6,11 +6,10 @@ export const useImportButtonData = () => {
const submitImportGradesButtonData = thunkActions.grades.useSubmitImportGradesButtonData();
const fileInputRef = useRef();
const hasFile = fileInputRef.current && fileInputRef.current.files[0];
const handleClickImportGrades = () => hasFile && fileInputRef.current.click();
const handleClickImportGrades = () => fileInputRef.current?.click();
const handleFileInputChange = () => {
if (hasFile) {
if (fileInputRef.current?.files[0]) {
const clearInput = () => {
fileInputRef.current.value = null;
};

View File

@@ -3,6 +3,7 @@ import { StrictDict } from 'utils';
const EMAIL_HEADING = 'Email';
const TOTAL_COURSE_GRADE_HEADING = 'Total Grade (%)';
const USERNAME_HEADING = 'Username';
const FULL_NAME_HEADING = 'Full Name';
const GradeFormats = StrictDict({
absolute: 'absolute',
@@ -10,15 +11,17 @@ const GradeFormats = StrictDict({
});
const Headings = StrictDict({
email: 'Email',
totalGrade: 'Total Grade (%)',
username: 'Username',
email: EMAIL_HEADING,
totalGrade: TOTAL_COURSE_GRADE_HEADING,
username: USERNAME_HEADING,
fullName: FULL_NAME_HEADING,
});
export {
EMAIL_HEADING,
TOTAL_COURSE_GRADE_HEADING,
USERNAME_HEADING,
FULL_NAME_HEADING,
GradeFormats,
Headings,
};

View File

@@ -92,7 +92,11 @@ export const formatMinAssignmentGrade = (percentGrade, options) => (
* @param {string} label - assignment filter label
* @return {string[]} - list of table headers
*/
export const headingMapper = (category, label = 'All') => {
export const headingMapper = (
category,
label = 'All',
hasMastersTrack = false,
) => {
const filters = {
all: section => section.label,
byCategory: section => section.label && section.category === category,
@@ -105,12 +109,25 @@ export const headingMapper = (category, label = 'All') => {
} else {
filter = filters.byLabel;
}
const { username, email, totalGrade } = Headings;
const {
username,
fullName,
email,
totalGrade,
} = Headings;
let userIdentificationHeadings;
if (hasMastersTrack) {
userIdentificationHeadings = [username, fullName, email];
} else {
userIdentificationHeadings = [username];
}
const filteredLabels = (entry) => entry.filter(filter).map(s => s.label);
return (entry) => (
entry
? [username, email, ...filteredLabels(entry), totalGrade]
? [...userIdentificationHeadings, ...filteredLabels(entry), totalGrade]
: []
);
};

View File

@@ -1,4 +1,9 @@
import { EMAIL_HEADING, TOTAL_COURSE_GRADE_HEADING, USERNAME_HEADING } from '../constants/grades';
import {
EMAIL_HEADING,
FULL_NAME_HEADING,
TOTAL_COURSE_GRADE_HEADING,
USERNAME_HEADING,
} from '../constants/grades';
import { formatDateForDisplay } from '../actions/utils';
import * as selectors from './grades';
import exportedSelectors from './grades';
@@ -177,34 +182,45 @@ describe('grades selectors', () => {
});
describe('headingMapper', () => {
const expectedHeaders = (subsectionLabels) => ([
const expectedMastersHeaders = (subsectionLabels) => ([
USERNAME_HEADING,
FULL_NAME_HEADING,
EMAIL_HEADING,
...subsectionLabels,
TOTAL_COURSE_GRADE_HEADING,
]);
const expectedNonMastersHeaders = (subsectionLabels) => ([
USERNAME_HEADING,
...subsectionLabels,
TOTAL_COURSE_GRADE_HEADING,
]);
const rows = genericResultsRows;
const selector = selectors.headingMapper;
it('creates headers for all assignments when no filtering is applied', () => {
expect(selector('All')(genericResultsRows)).toEqual(
expectedHeaders([rows[0].label, rows[1].label, rows[2].label]),
);
});
it('creates headers for only matching assignment types when type filter is applied', () => {
expect(
selector('Homework')(genericResultsRows),
).toEqual(
expectedHeaders([rows[0].label, rows[1].label]),
);
});
it('creates headers for only matching assignment when label filter is applied', () => {
expect(selector('Homework', rows[1].label)(rows)).toEqual(
expectedHeaders([rows[1].label]),
);
});
it('returns an empty array when no entries are passed', () => {
expect(selector('all')(undefined)).toEqual([]);
[true, false].forEach((isMasters) => {
const expectedHeaders = isMasters ? expectedMastersHeaders : expectedNonMastersHeaders;
describe(isMasters ? 'Masters' : 'Not Masters', () => {
it('creates headers for all assignments when no filtering is applied', () => {
expect(selector('All', 'All', isMasters)(genericResultsRows)).toEqual(
expectedHeaders([rows[0].label, rows[1].label, rows[2].label]),
);
});
it('creates headers for only matching assignment types when type filter is applied', () => {
expect(
selector('Homework', 'All', isMasters)(genericResultsRows),
).toEqual(
expectedHeaders([rows[0].label, rows[1].label]),
);
});
it('creates headers for only matching assignment when label filter is applied', () => {
expect(selector('Homework', rows[1].label, isMasters)(rows)).toEqual(
expectedHeaders([rows[1].label]),
);
});
it('returns an empty array when no entries are passed', () => {
expect(selector('all')(undefined)).toEqual([]);
});
});
});
});

View File

@@ -109,6 +109,7 @@ export const formattedGradeLimits = (state) => {
export const getHeadings = (state) => grades.headingMapper(
filters.assignmentType(state) || 'All',
filters.selectedAssignmentLabel(state) || 'All',
tracks.stateHasMastersTrack(state),
)(grades.getExampleSectionBreakdown(state));
/**

View File

@@ -295,20 +295,27 @@ describe('root selectors', () => {
const selector = moduleSelectors.getHeadings;
beforeEach(() => {
selectors.grades.headingMapper = jest.fn(
(type, label) => (breakdown) => ({ headingMapper: { type, label, breakdown } }),
(type, label, hasMastersTrack) => (breakdown) => ({
headingMapper: {
type, label, hasMastersTrack, breakdown,
},
}),
);
selectors.filters.assignmentType = jest.fn();
selectors.filters.selectedAssignmentLabel = jest.fn();
selectors.tracks.stateHasMastersTrack = jest.fn();
selectors.grades.getExampleSectionBreakdown = mockFn('getExampleSectionBreakdown');
});
describe('no assignmentType or label selected', () => {
it('maps selected filters into getExampleSectionBreakdown', () => {
selectors.filters.assignmentType.mockReturnValue(undefined);
selectors.filters.selectedAssignmentLabel.mockReturnValue(undefined);
selectors.tracks.stateHasMastersTrack.mockReturnValue(false);
expect(selector(testState)).toEqual({
headingMapper: {
type: 'All',
label: 'All',
hasMastersTrack: false,
breakdown: { getExampleSectionBreakdown: testState },
},
});
@@ -318,10 +325,27 @@ describe('root selectors', () => {
it('maps selected filters into getExampleSectionBreakdown', () => {
selectors.filters.assignmentType.mockReturnValue(mockAssignmentType);
selectors.filters.selectedAssignmentLabel.mockReturnValue(mockAssignmentLabel);
selectors.tracks.stateHasMastersTrack.mockReturnValue(false);
expect(selector(testState)).toEqual({
headingMapper: {
type: mockAssignmentType,
label: mockAssignmentLabel,
hasMastersTrack: false,
breakdown: { getExampleSectionBreakdown: testState },
},
});
});
});
describe('has masters track', () => {
it('maps selected filters into getExampleSectionBreakdown', () => {
selectors.filters.assignmentType.mockReturnValue(undefined);
selectors.filters.selectedAssignmentLabel.mockReturnValue(undefined);
selectors.tracks.stateHasMastersTrack.mockReturnValue(true);
expect(selector(testState)).toEqual({
headingMapper: {
type: 'All',
label: 'All',
hasMastersTrack: true,
breakdown: { getExampleSectionBreakdown: testState },
},
});