feat: enable teams support (#35)
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
***********************************/
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user