feat: enable teams support (#35)

This commit is contained in:
Ben Warzeski
2021-12-03 11:22:15 -05:00
committed by GitHub
parent 97750dd525
commit 909516dbc7
9 changed files with 305 additions and 54 deletions

View File

@@ -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 = {

View File

@@ -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',

View File

@@ -121,3 +121,123 @@ exports[`SubmissionsTable component component render tests snapshots snapshot: h
<DataTable.TableFooter />
</DataTable>
`;
exports[`SubmissionsTable component component render tests snapshots snapshot: team happy path 1`] = `
<DataTable
bulkActions={
Array [
[MockFunction this.selectedBulkAction],
]
}
columns={
Array [
Object {
"Header": "Team name",
"accessor": "teamName",
},
Object {
"Cell": [MockFunction this.formatDate],
"Header": "Team submission date",
"accessor": "dateSubmitted",
"disableFilters": true,
},
Object {
"Cell": [MockFunction this.formatGrade],
"Header": "Grade",
"accessor": "score",
"disableFilters": true,
},
Object {
"Cell": [MockFunction this.formatStatus],
"Filter": "MultiSelectDropdownFilter",
"Header": "Grading status",
"accessor": "gradingStatus",
"filter": "includesValue",
"filterChoices": Array [
Object {
"name": "Ungraded",
"value": "ungraded",
},
Object {
"name": "Grading Completed",
"value": "graded",
},
Object {
"name": "Currently being graded by someone else",
"value": "locked",
},
Object {
"name": "You are currently grading this response",
"value": "in-progress",
},
],
},
]
}
data={
Array [
Object {
"dateSubmitted": 16131215154955,
"gradingStatus": "ungraded",
"score": Object {
"pointsEarned": 1,
"pointsPossible": 10,
},
"teamName": "teamName-1",
},
Object {
"dateSubmitted": 16131225154955,
"gradingStatus": "graded",
"score": Object {
"pointsEarned": 2,
"pointsPossible": 10,
},
"teamName": "teamName-2",
},
Object {
"dateSubmitted": 16131215250955,
"gradingStatus": "in-progress",
"score": Object {
"pointsEarned": 3,
"pointsPossible": 10,
},
"teamName": "teamName-3",
},
]
}
defaultColumnValues={
Object {
"Filter": "TextFilter",
}
}
initialState={
Object {
"pageIndex": 0,
"pageSize": 10,
}
}
isFilterable={true}
isPaginated={true}
isSelectable={true}
isSortable={true}
itemCount={3}
numBreakoutFilters={2}
tableActions={
Array [
Object {
"buttonText": "View all responses",
"className": "view-all-responses-btn",
"handleClick": [MockFunction this.handleViewAllResponsesClick],
"variant": "primary",
},
]
}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.EmptyTable
content="No results found"
/>
<DataTable.TableFooter />
</DataTable>
`;

View File

@@ -11,7 +11,7 @@ exports[`ReviewActions component component snapshot: do not show rubric 1`] = `
<span
className="lead"
>
test-username
test-userDisplay
</span>
<StatusBadge
className="review-actions-status mr-3"
@@ -64,7 +64,7 @@ exports[`ReviewActions component component snapshot: loading 1`] = `
<span
className="lead"
>
test-username
test-userDisplay
</span>
<StatusBadge
className="review-actions-status mr-3"
@@ -106,7 +106,7 @@ exports[`ReviewActions component component snapshot: show rubric, no score 1`] =
<span
className="lead"
>
test-username
test-userDisplay
</span>
<StatusBadge
className="review-actions-status mr-3"

View File

@@ -20,13 +20,13 @@ export const ReviewActions = ({
toggleShowRubric,
score: { pointsEarned, pointsPossible },
showRubric,
username,
userDisplay,
isLoaded,
}) => (
<div>
<ActionRow className="review-actions">
<span className="review-actions-username">
<span className="lead">{username}</span>
<span className="lead">{userDisplay}</span>
{ gradingStatus && (
<StatusBadge className="review-actions-status mr-3" status={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),

View File

@@ -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));

View File

@@ -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),
};
/**

View File

@@ -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
***********************************/

View File

@@ -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',
});