From 909516dbc7daa0035530da944ec35e8f4f6533f8 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Fri, 3 Dec 2021 11:22:15 -0500 Subject: [PATCH] feat: enable teams support (#35) --- src/containers/ListView/SubmissionsTable.jsx | 32 +++- .../ListView/SubmissionsTable.test.jsx | 151 ++++++++++++++---- .../SubmissionsTable.test.jsx.snap | 120 ++++++++++++++ .../__snapshots__/index.test.jsx.snap | 6 +- src/containers/ReviewActions/index.jsx | 8 +- src/containers/ReviewActions/index.test.jsx | 8 +- src/data/redux/app/selectors.js | 7 +- src/data/redux/grading/selectors.js | 14 ++ src/data/services/lms/constants.js | 13 ++ 9 files changed, 305 insertions(+), 54 deletions(-) diff --git a/src/containers/ListView/SubmissionsTable.jsx b/src/containers/ListView/SubmissionsTable.jsx index 5b89b51..fe4f2a6 100644 --- a/src/containers/ListView/SubmissionsTable.jsx +++ b/src/containers/ListView/SubmissionsTable.jsx @@ -9,7 +9,7 @@ import { } from '@edx/paragon'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { gradingStatuses } from 'data/services/lms/constants'; +import { gradingStatuses, submissionFields } from 'data/services/lms/constants'; import lmsMessages from 'data/services/lms/messages'; import { selectors, thunkActions } from 'data/redux'; @@ -35,6 +35,22 @@ export class SubmissionsTable extends React.Component { })); } + get userLabel() { + return this.translate(this.props.isIndividual ? messages.username : messages.teamName); + } + + get userAccessor() { + return this.props.isIndividual + ? submissionFields.username + : submissionFields.teamName; + } + + get dateSubmittedLabel() { + return this.translate(this.props.isIndividual + ? messages.learnerSubmissionDate + : messages.teamSubmissionDate); + } + formatDate = ({ value }) => { const date = new Date(value); return date.toLocaleString(); @@ -94,24 +110,24 @@ export class SubmissionsTable extends React.Component { ]} columns={[ { - Header: this.translate(messages.username), - accessor: 'username', + Header: this.userLabel, + accessor: this.userAccessor, }, { - Header: this.translate(messages.learnerSubmissionDate), - accessor: 'dateSubmitted', + Header: this.dateSubmittedLabel, + accessor: submissionFields.dateSubmitted, Cell: this.formatDate, disableFilters: true, }, { Header: this.translate(messages.grade), - accessor: 'score', + accessor: submissionFields.score, Cell: this.formatGrade, disableFilters: true, }, { Header: this.translate(messages.gradingStatus), - accessor: 'gradingStatus', + accessor: submissionFields.gradingStatus, Cell: this.formatStatus, Filter: MultiSelectDropdownFilter, filter: 'includesValue', @@ -134,6 +150,7 @@ SubmissionsTable.propTypes = { // injected intl: intlShape.isRequired, // redux + isIndividual: PropTypes.bool.isRequired, listData: PropTypes.arrayOf(PropTypes.shape({ username: PropTypes.string, dateSubmitted: PropTypes.number, @@ -148,6 +165,7 @@ SubmissionsTable.propTypes = { export const mapStateToProps = (state) => ({ listData: selectors.submissions.listData(state), + isIndividual: selectors.app.ora.isIndividual(state), }); export const mapDispatchToProps = { diff --git a/src/containers/ListView/SubmissionsTable.test.jsx b/src/containers/ListView/SubmissionsTable.test.jsx index 2610dad..003974a 100644 --- a/src/containers/ListView/SubmissionsTable.test.jsx +++ b/src/containers/ListView/SubmissionsTable.test.jsx @@ -8,7 +8,7 @@ import { } from '@edx/paragon'; import { selectors, thunkActions } from 'data/redux'; -import { gradingStatuses as statuses } from 'data/services/lms/constants'; +import { gradingStatuses as statuses, submissionFields } from 'data/services/lms/constants'; import StatusBadge from 'components/StatusBadge'; import { formatMessage } from 'testUtils'; @@ -21,6 +21,11 @@ import { jest.mock('data/redux', () => ({ selectors: { + app: { + ora: { + isIndividual: (...args) => ({ isIndividual: args }), + }, + }, submissions: { listData: (...args) => ({ listData: args }), }, @@ -35,38 +40,71 @@ jest.mock('data/redux', () => ({ let el; jest.useFakeTimers('modern'); +const individualData = [ + { + username: 'username-1', + dateSubmitted: 16131215154955, + gradingStatus: statuses.ungraded, + score: { + pointsEarned: 1, + pointsPossible: 10, + }, + }, + { + username: 'username-2', + dateSubmitted: 16131225154955, + gradingStatus: statuses.graded, + score: { + pointsEarned: 2, + pointsPossible: 10, + }, + }, + { + username: 'username-3', + dateSubmitted: 16131215250955, + gradingStatus: statuses.inProgress, + score: { + pointsEarned: 3, + pointsPossible: 10, + }, + }, +]; + +const teamData = [ + { + teamName: 'teamName-1', + dateSubmitted: 16131215154955, + gradingStatus: statuses.ungraded, + score: { + pointsEarned: 1, + pointsPossible: 10, + }, + }, + { + teamName: 'teamName-2', + dateSubmitted: 16131225154955, + gradingStatus: statuses.graded, + score: { + pointsEarned: 2, + pointsPossible: 10, + }, + }, + { + teamName: 'teamName-3', + dateSubmitted: 16131215250955, + gradingStatus: statuses.inProgress, + score: { + pointsEarned: 3, + pointsPossible: 10, + }, + }, +]; + describe('SubmissionsTable component', () => { describe('component', () => { const props = { - listData: [ - { - username: 'username-1', - dateSubmitted: 16131215154955, - gradingStatus: statuses.ungraded, - score: { - pointsEarned: 1, - pointsPossible: 10, - }, - }, - { - username: 'username-2', - dateSubmitted: 16131225154955, - gradingStatus: statuses.graded, - score: { - pointsEarned: 2, - pointsPossible: 10, - }, - }, - { - username: 'username-3', - dateSubmitted: 16131215250955, - gradingStatus: statuses.inProgress, - score: { - pointsEarned: 3, - pointsPossible: 10, - }, - }, - ], + isIndividual: true, + listData: [...individualData], }; beforeEach(() => { props.loadSelectionForReview = jest.fn(); @@ -95,6 +133,10 @@ describe('SubmissionsTable component', () => { test('snapshot: happy path', () => { expect(el.instance().render()).toMatchSnapshot(); }); + test('snapshot: team happy path', () => { + el.setProps({ isIndividual: false, listData: [...teamData] }); + expect(el.instance().render()).toMatchSnapshot(); + }); }); describe('DataTable', () => { let table; @@ -118,7 +160,7 @@ describe('SubmissionsTable component', () => { test('bulkActions linked to selectedBulkAction', () => { expect(tableProps.bulkActions).toEqual([el.instance().selectedBulkAction]); }); - describe('columns', () => { + describe('individual columns', () => { let columns; beforeEach(() => { columns = tableProps.columns; @@ -126,13 +168,13 @@ describe('SubmissionsTable component', () => { test('username column', () => { expect(columns[0]).toEqual({ Header: messages.username.defaultMessage, - accessor: 'username', + accessor: submissionFields.username, }); }); test('submission date column', () => { expect(columns[1]).toEqual({ Header: messages.learnerSubmissionDate.defaultMessage, - accessor: 'dateSubmitted', + accessor: submissionFields.dateSubmitted, Cell: el.instance().formatDate, disableFilters: true, }); @@ -140,7 +182,7 @@ describe('SubmissionsTable component', () => { test('grade column', () => { expect(columns[2]).toEqual({ Header: messages.grade.defaultMessage, - accessor: 'score', + accessor: submissionFields.score, Cell: el.instance().formatGrade, disableFilters: true, }); @@ -148,7 +190,46 @@ describe('SubmissionsTable component', () => { test('grading status column', () => { expect(columns[3]).toEqual({ Header: messages.gradingStatus.defaultMessage, - accessor: 'gradingStatus', + accessor: submissionFields.gradingStatus, + Cell: el.instance().formatStatus, + Filter: MultiSelectDropdownFilter, + filter: 'includesValue', + filterChoices: el.instance().gradeStatusOptions, + }); + }); + }); + describe('team columns', () => { + let columns; + beforeEach(() => { + el.setProps({ isIndividual: false, listData: [...teamData] }); + columns = el.find(DataTable).props().columns; + }); + test('teamName column', () => { + expect(columns[0]).toEqual({ + Header: messages.teamName.defaultMessage, + accessor: submissionFields.teamName, + }); + }); + test('submission date column', () => { + expect(columns[1]).toEqual({ + Header: messages.teamSubmissionDate.defaultMessage, + accessor: submissionFields.dateSubmitted, + Cell: el.instance().formatDate, + disableFilters: true, + }); + }); + test('grade column', () => { + expect(columns[2]).toEqual({ + Header: messages.grade.defaultMessage, + accessor: submissionFields.score, + Cell: el.instance().formatGrade, + disableFilters: true, + }); + }); + test('grading status column', () => { + expect(columns[3]).toEqual({ + Header: messages.gradingStatus.defaultMessage, + accessor: submissionFields.gradingStatus, Cell: el.instance().formatStatus, Filter: MultiSelectDropdownFilter, filter: 'includesValue', diff --git a/src/containers/ListView/__snapshots__/SubmissionsTable.test.jsx.snap b/src/containers/ListView/__snapshots__/SubmissionsTable.test.jsx.snap index 5c373df..5bd942a 100644 --- a/src/containers/ListView/__snapshots__/SubmissionsTable.test.jsx.snap +++ b/src/containers/ListView/__snapshots__/SubmissionsTable.test.jsx.snap @@ -121,3 +121,123 @@ exports[`SubmissionsTable component component render tests snapshots snapshot: h `; + +exports[`SubmissionsTable component component render tests snapshots snapshot: team happy path 1`] = ` + + + + + + +`; diff --git a/src/containers/ReviewActions/__snapshots__/index.test.jsx.snap b/src/containers/ReviewActions/__snapshots__/index.test.jsx.snap index 2331a6e..0d7a7f5 100644 --- a/src/containers/ReviewActions/__snapshots__/index.test.jsx.snap +++ b/src/containers/ReviewActions/__snapshots__/index.test.jsx.snap @@ -11,7 +11,7 @@ exports[`ReviewActions component component snapshot: do not show rubric 1`] = ` - test-username + test-userDisplay - test-username + test-userDisplay - test-username + test-userDisplay (
- {username} + {userDisplay} { gradingStatus && ( )} @@ -59,7 +59,7 @@ ReviewActions.defaultProps = { }; ReviewActions.propTypes = { gradingStatus: PropTypes.string, - username: PropTypes.string.isRequired, + userDisplay: PropTypes.string.isRequired, score: PropTypes.shape({ pointsEarned: PropTypes.number, pointsPossible: PropTypes.number, @@ -70,7 +70,7 @@ ReviewActions.propTypes = { }; export const mapStateToProps = (state) => ({ - username: selectors.grading.selected.username(state), + userDisplay: selectors.grading.selected.userDisplay(state), gradingStatus: selectors.grading.selected.gradingStatus(state), score: selectors.grading.selected.score(state), showRubric: selectors.app.showRubric(state), diff --git a/src/containers/ReviewActions/index.test.jsx b/src/containers/ReviewActions/index.test.jsx index b2e8023..2999828 100644 --- a/src/containers/ReviewActions/index.test.jsx +++ b/src/containers/ReviewActions/index.test.jsx @@ -13,7 +13,7 @@ jest.mock('data/redux/grading/selectors', () => ({ selected: { gradingStatus: (state) => ({ gradingStatus: state }), score: (state) => ({ score: state }), - username: (state) => ({ username: state }), + userDisplay: (state) => ({ userDisplay: state }), }, })); jest.mock('data/redux/requests/selectors', () => ({ @@ -27,7 +27,7 @@ describe('ReviewActions component', () => { describe('component', () => { const props = { gradingStatus: 'grading-status', - username: 'test-username', + userDisplay: 'test-userDisplay', showRubric: false, score: { pointsEarned: 3, pointsPossible: 10 }, }; @@ -54,8 +54,8 @@ describe('ReviewActions component', () => { const requestKey = RequestKeys.fetchSubmission; expect(mapped.isLoaded).toEqual(selectors.requests.isCompleted(testState, { requestKey })); }); - test('username loads from grading.selected.username', () => { - expect(mapped.username).toEqual(selectors.grading.selected.username(testState)); + test('userDisplay loads from grading.selected.userDisplay', () => { + expect(mapped.userDisplay).toEqual(selectors.grading.selected.userDisplay(testState)); }); test('gradingStatus loads from grading.selected.gradingStatus', () => { expect(mapped.gradingStatus).toEqual(selectors.grading.selected.gradingStatus(testState)); diff --git a/src/data/redux/app/selectors.js b/src/data/redux/app/selectors.js index a6c3a4a..100e4d5 100644 --- a/src/data/redux/app/selectors.js +++ b/src/data/redux/app/selectors.js @@ -1,6 +1,6 @@ import { createSelector } from 'reselect'; -import { feedbackRequirement } from 'data/services/lms/constants'; +import { feedbackRequirement, oraTypes } from 'data/services/lms/constants'; import { StrictDict } from 'utils'; @@ -46,6 +46,11 @@ export const ora = { * @return {string} - file upload response config */ fileUploadResponseConfig: oraMetadataSelector(data => data.fileUploadResponseConfig), + /** + * Returns true iff the ORA is an individual submission ora (vs team) + * @return {bool} - is the ORA an individual ORA? + */ + isIndividual: oraMetadataSelector(data => data.type === oraTypes.individual), }; /** diff --git a/src/data/redux/grading/selectors.js b/src/data/redux/grading/selectors.js index 070a577..12f1b00 100644 --- a/src/data/redux/grading/selectors.js +++ b/src/data/redux/grading/selectors.js @@ -113,6 +113,20 @@ selected.username = createSelector( (staticData) => staticData.username, ); +selected.teamName = createSelector( + [module.selected.staticData], + (staticData) => staticData.teamName, +); + +selected.userDisplay = createSelector( + [ + appSelectors.ora.isIndividual, + module.selected.username, + module.selected.teamName, + ], + (isIndividual, username, teamName) => (isIndividual ? username : teamName), +); + /*********************************** * Selected Submission - Grade Data ***********************************/ diff --git a/src/data/services/lms/constants.js b/src/data/services/lms/constants.js index 00c1bf3..76f7657 100644 --- a/src/data/services/lms/constants.js +++ b/src/data/services/lms/constants.js @@ -34,3 +34,16 @@ export const paramKeys = StrictDict({ oraLocation: 'oraLocation', submissionUUID: 'submissionUUID', }); + +export const oraTypes = StrictDict({ + team: 'team', + individual: 'individual', +}); + +export const submissionFields = StrictDict({ + dateSubmitted: 'dateSubmitted', + gradingStatus: 'gradingStatus', + score: 'score', + teamName: 'teamName', + username: 'username', +});