incorporate leangseu rubric and navigation PR

This commit is contained in:
Ben Warzeski
2021-09-21 17:08:38 -04:00
parent a259204fce
commit fe6753b259
21 changed files with 422 additions and 175 deletions

View File

@@ -0,0 +1,60 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
Card,
} from '@edx/paragon';
import createDOMPurify from 'dompurify';
import parse from 'html-react-parser';
import selectors from 'data/selectors';
/**
* <ResponseDisplay />
*/
export class ResponseDisplay extends React.Component {
constructor(props) {
super(props);
this.purify = createDOMPurify(window);
}
get textContent() {
return parse(this.purify.sanitize(this.props.response.text));
}
get hasResponse() {
return this.props.response !== undefined;
}
render() {
return (
<Card className="response-card">
{this.hasResponse && (
<Card.Body>
{this.textContent}
</Card.Body>
)}
</Card>
);
}
}
ResponseDisplay.defaultProps = {
};
ResponseDisplay.propTypes = {
response: PropTypes.shape({
text: PropTypes.string,
}).isRequired,
};
export const mapStateToProps = (state) => ({
response: selectors.grading.selectedResponse(state),
});
export const mapDispatchToProps = {
};
export default connect(mapStateToProps, mapDispatchToProps)(ResponseDisplay);

View File

@@ -85,7 +85,7 @@ export class ListView extends React.Component {
},
{
Header: 'Grade',
accessor: 'grade',
accessor: 'score',
Cell: this.formatGrade,
},
{

View File

@@ -6,33 +6,51 @@ import {
ActionRow,
Button,
} from '@edx/paragon';
import { Edit } from '@edx/paragon/icons';
import actions from 'data/actions';
import selectors from 'data/selectors';
import StatusBadge from 'components/StatusBadge';
import SubmissionNavigation from './SubmissionNavigation';
import './ReviewModal.scss';
export const ReviewActions = ({ gradeStatus, username }) => (
export const ReviewActions = ({
gradeStatus,
toggleShowRubric,
showRubric,
username,
}) => (
<div className="review-actions">
<ActionRow>
<span className="review-actions-username">{username}</span>
<StatusBadge className="review-actions-status" status={gradeStatus} />
<span className="review-actions-username">
{username}
<StatusBadge className="ml-3 review-actions-status" status={gradeStatus} />
</span>
<ActionRow.Spacer />
<Button>Show Rubric</Button>
<Button variant="outline-primary" onClick={toggleShowRubric}>
{showRubric ? 'Hide' : 'Show'} Rubric
</Button>
<Button variant="primary" iconAfter={Edit}>Start Grading</Button>
<SubmissionNavigation />
</ActionRow>
</div>
);
ReviewActions.propTypes = {
gradeStatus: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
showRubric: PropTypes.bool.isRequired,
toggleShowRubric: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
username: selectors.grading.selected.username(state),
gradeStatus: selectors.grading.selected.gradeStatus(state),
showRubric: selectors.app.showRubric(state),
});
export const mapDispatchToProps = {
toggleShowRubric: actions.app.toggleShowRubric,
};
export default connect(mapStateToProps, mapDispatchToProps)(ReviewActions);

View File

@@ -1,7 +1,84 @@
@import "@edx/paragon/scss/core/core";
.review-actions {
padding: map_get($spacers, 3);
@include media-breakpoint-down(md) {
.review-actions {
flex-direction: column;
align-items: flex-start !important;
& > * {
margin-bottom: map_get($spacers, 1) !important;
}
}
}
#ora-esg-list-view {
padding: 20px;
.review-modal {
background-color: $gray-300 !important;
padding: inherit;
// action reviews
.review-actions {
padding: map_get($spacers, 3);
flex-direction: row;
background-color: $light-200;
.review-actions-username {
flex-grow: 1;
}
}
// text response
.response-card {
padding: map-get($map: $spacers, $key: 0);
max-width: map-get($map: $container-max-widths, $key: "sm");
height: 100%;
overflow-y: scroll;
}
}
.content-block {
width: fit-content;
margin: auto;
height: 100%;
}
@include media-breakpoint-down(sm) {
.fullscreen-modal-body {
padding: 0 !important;
overflow-y: hidden !important;
& > div:nth-child(2) {
height: 100%;
.row,
.col {
height: 100%;
padding: 0;
}
}
}
}
.grading-rubric-card {
width: 320px;
height: fit-content;
max-height: 70vh;
min-height: 320px;
.grading-rubric-header {
box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.3) !important;
display: flex;
justify-content: center;
padding: map-get($map: $spacers, $key: 3);
}
.grading-rubric-body {
overflow-y: scroll;
}
.grading-rubric-footer {
box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.3) !important;
display: flex;
justify-content: center;
padding: map-get($map: $spacers, $key: 3);
}
}

View File

@@ -0,0 +1,63 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Icon, IconButton } from '@edx/paragon';
import { ChevronLeft, ChevronRight } from '@edx/paragon/icons';
import selectors from 'data/selectors';
import thunkActions from 'data/thunkActions';
/**
* <SubmissionNavigation />
*/
export const SubmissionNavigation = ({
hasPrevSubmission,
hasNextSubmission,
loadPrev,
loadNext,
activeIndex,
selectionLength,
}) => (
<>
<IconButton
disabled={!hasPrevSubmission}
alt="Load previous submission"
src={ChevronLeft}
iconAs={Icon}
onClick={loadPrev}
/>
<span>{activeIndex + 1} of {selectionLength}</span>
<IconButton
disabled={!hasNextSubmission}
alt="Load next submission"
src={ChevronRight}
iconAs={Icon}
onClick={loadNext}
/>
</>
);
SubmissionNavigation.defaultProps = {
};
SubmissionNavigation.propTypes = {
hasPrevSubmission: PropTypes.bool.isRequired,
hasNextSubmission: PropTypes.bool.isRequired,
loadPrev: PropTypes.func.isRequired,
loadNext: PropTypes.func.isRequired,
activeIndex: PropTypes.number.isRequired,
selectionLength: PropTypes.number.isRequired,
};
export const mapStateToProps = (state) => ({
hasPrevSubmission: selectors.grading.hasPrevSubmission(state),
hasNextSubmission: selectors.grading.hasNextSubmission(state),
activeIndex: selectors.grading.activeIndex(state),
selectionLength: selectors.grading.selectionLength(state),
});
export const mapDispatchToProps = {
loadPrev: thunkActions.grading.loadPrev,
loadNext: thunkActions.grading.loadNext,
};
export default connect(mapStateToProps, mapDispatchToProps)(SubmissionNavigation);

View File

@@ -4,16 +4,17 @@ import { connect } from 'react-redux';
import {
FullscreenModal,
Container,
Row,
Col,
} from '@edx/paragon';
import createDOMPurify from 'dompurify';
import parse from 'html-react-parser';
import selectors from 'data/selectors';
import actions from 'data/actions';
import thunkActions from 'data/thunkActions';
import ResponseDisplay from 'components/ResponseDisplay';
import Rubric from 'containers/Rubric';
import ReviewActions from './ReviewActions';
import './ReviewModal.scss';
@@ -24,16 +25,9 @@ import './ReviewModal.scss';
export class ReviewModal extends React.Component {
constructor(props) {
super(props);
console.log('review modal');
console.log({ props });
this.purify = createDOMPurify(window);
this.onClose = this.onClose.bind(this);
}
get textContent() {
return parse(this.purify.sanitize(this.props.response.text));
}
onClose() {
this.props.setShowReview(false);
}
@@ -48,10 +42,14 @@ export class ReviewModal extends React.Component {
isOpen={this.props.isOpen}
beforeBodyNode={<ReviewActions />}
onClose={this.onClose}
className="review-modal"
>
<Container size="md">
{this.textContent}
</Container>
<div className="content-block">
<Row>
<Col><ResponseDisplay /></Col>
{ this.props.showRubric && <Rubric /> }
</Row>
</div>
</FullscreenModal>
);
}
@@ -66,12 +64,14 @@ ReviewModal.propTypes = {
text: PropTypes.node,
}),
setShowReview: PropTypes.func.isRequired,
showRubric: PropTypes.bool.isRequired,
};
export const mapStateToProps = (state) => ({
isOpen: selectors.app.showReview(state),
oraName: selectors.app.oraName(state),
response: selectors.grading.selected.response(state),
showRubric: selectors.app.showRubric(state),
});
export const mapDispatchToProps = {

View File

@@ -0,0 +1,93 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
Card,
Button,
Form,
} from '@edx/paragon';
/**
* <GradingRubric />
*/
export const GradingRubric = ({
}) => {
return (
<Card className="grading-rubric-card">
<Card.Body className="grading-rubric-body">
<h3>Rubric</h3>
<hr />
<Form.Group>
<Form.Label>Which Color?</Form.Label>
<Form.RadioSet
name="colors"
>
<Form.Radio value="red">Red</Form.Radio>
<Form.Radio value="green">Green</Form.Radio>
<Form.Radio value="blue">Blue</Form.Radio>
<Form.Radio value="cyan" disabled>Cyan</Form.Radio>
</Form.RadioSet>
</Form.Group>
<Form.Group>
<Form.Control
floatingLabel="Comments"
/>
</Form.Group>
<Form.Group isInvalid>
<Form.Label>Which Color?</Form.Label>
<Form.RadioSet
name="colors"
>
<Form.Radio value="red">Red</Form.Radio>
<Form.Radio value="green">Green</Form.Radio>
<Form.Radio value="blue">Blue</Form.Radio>
<Form.Radio value="cyan" disabled>Cyan</Form.Radio>
</Form.RadioSet>
<Form.Control.Feedback type="invalid">
Make a selection
</Form.Control.Feedback>
</Form.Group>
<Form.Group isInvalid>
<Form.Control
floatingLabel="Comments"
/>
<Form.Control.Feedback type="invalid">
Make a comment
</Form.Control.Feedback>
</Form.Group>
<Form.Group>
<Form.Label>Which Color?</Form.Label>
<Form.RadioSet
name="colors"
>
<Form.Radio value="red">Red</Form.Radio>
<Form.Radio value="green">Green</Form.Radio>
<Form.Radio value="blue">Blue</Form.Radio>
<Form.Radio value="cyan" disabled>Cyan</Form.Radio>
</Form.RadioSet>
</Form.Group>
<Form.Group>
<Form.Control
floatingLabel="Comments"
/>
</Form.Group>
</Card.Body>
<div className="grading-rubric-footer">
<Button>Submit Grade</Button>
</div>
</Card>
);
}
GradingRubric.defaultProps = {};
GradingRubric.propTypes = {};
export const mapStateToProps = (state) => ({
});
export const mapDispatchToProps = {
};
export default connect(mapStateToProps, mapDispatchToProps)(GradingRubric);

View File

@@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
Card,
Button,
} from '@edx/paragon';
/**
* <GradingRubric />
*/
export const Rubric = ({
}) => {
return (
<Card className="grading-rubric-card">
<Card.Body className="grading-rubric-body" />
<div className="grading-rubric-footer">
<Button>Submit Grade</Button>
</div>
</Card>
);
}
Rubric.defaultProps = {};
Rubric.propTypes = {};
export const mapStateToProps = (state) => ({
});
export const mapDispatchToProps = {
};
export default connect(mapStateToProps, mapDispatchToProps)(Rubric);

View File

@@ -8,10 +8,12 @@ export const loadCourseMetadata = createAction('loadCourseMetadata');
export const loadOraMetadata = createAction('loadOraMetadata');
export const setGrading = createAction('setGrading');
export const setShowReview = createAction('setReview');
export const toggleShowRubric = createAction('toggleShowRubric');
export default StrictDict({
loadCourseMetadata,
loadOraMetadata,
setGrading,
setShowReview,
toggleShowRubric,
});

View File

@@ -10,7 +10,6 @@ const createAction = createActionFactory(dataKey);
* @param {obj} submission data for the review/grading view
* {
* {obj} response - api response data
* {obj} staticData - api submission static data
* {obj} gradeData - api grade data
* {str} status - api grade status
* }
@@ -20,14 +19,14 @@ const loadSubmission = createAction('loadSubmission');
/**
* Pre-load just the static info about the "next" submission in the review queue.
* Load submission and the learner's response.
* @param {obj} submission ({ staticData, response })
* @param {obj} submission ({ response })
*/
const preloadNext = createAction('preloadNext');
/**
* Pre-load just the static info about the "previous" submission in the review queue.
* Load submission and the learner's response.
* @param {obj} submission ({ staticData, response })
* @param {obj} submission ({ response })
*/
const preloadPrev = createAction('preloadPrev');

View File

@@ -13,6 +13,7 @@ const initialState = {
org: '',
},
showReview: false,
showRubric: false,
grading: false,
};
@@ -27,6 +28,8 @@ const app = (state = initialState, { type, payload }) => {
return { ...state, showReview: payload };
case actions.app.setGrading.toString():
return { ...state, grading: payload };
case actions.app.toggleShowRubric.toString():
return { ...state, showRubric: !state.showRubric };
default:
return state;
}

View File

@@ -75,6 +75,7 @@ const app = (state = initialState, { type, payload }) => {
...state,
prev: state.current,
current: {
response: state.next.response,
...payload,
},
activeIndex: state.activeIndex + 1,
@@ -85,6 +86,7 @@ const app = (state = initialState, { type, payload }) => {
...state,
next: state.current,
current: {
response: state.prev.response,
...payload,
},
activeIndex: state.activeIndex - 1,

View File

@@ -2,6 +2,7 @@ import { StrictDict } from 'utils';
export const simpleSelectors = {
showReview: state => state.app.showReview,
showRubric: state => state.app.showRubric,
grading: state => state.app.grading,
courseMetadata: state => state.app.courseMetadata,
oraName: state => state.app.oraMetadata.name,

View File

@@ -10,6 +10,15 @@ export const simpleSelectors = {
current: state => state.grading.current,
};
/**
* returns the length of the list of selected submissions
* @return {number} selected submission list length
*/
export const selectionLength = createSelector(
[module.simpleSelectors.selected],
(selected) => selected.length,
);
/**
* returns the selected submission id
* @return {string} selected submission id
@@ -136,4 +145,6 @@ export default StrictDict({
nextSubmissionId,
prevSubmissionId,
selected,
selectedResponse,
selectionLength,
});

View File

@@ -2,13 +2,9 @@ import _ from 'lodash';
import { createSelector } from 'reselect';
import { StrictDict } from 'utils';
import * as module from './submissions';
export const simpleSelectors = {
list: state => state.submissions.list,
selected: state => state.submissions.selected,
activeIndex: state => state.submissions.activeIndex,
current: state => state.submissions.current,
};
/**
@@ -22,131 +18,7 @@ export const listData = createSelector(
),
);
/**
* returns the selected submission id
* @return {string} selected submission id
*/
export const selectedSubmissionId = createSelector(
[module.simpleSelectors.selected, module.simpleSelectors.activeIndex],
(selected, index) => selected[index],
);
/**
* returns static data from the active selected submission
* @return {obj} - staticData
* { submissionId, username, teamName, dateSubmitted }
*/
export const selectedStaticData = createSelector(
[module.selectedSubmissionId, module.simpleSelectors.list],
(submissionId, list) => {
const submission = list[submissionId];
const { grade, gradeStatus, ...staticData } = submission;
return staticData;
},
);
/**
* Returns the username for the selected submission
* @return {string} username
*/
export const selectedUsername = createSelector(
[module.selectedStaticData],
(staticData) => staticData.username,
);
/**
* Returns the grade status for the selected submission
* @return {string} grade status
*/
export const selectedGradeStatus = createSelector(
[module.simpleSelectors.current],
(current) => current.gradeStatus,
);
/**
* Returns the grade data for the selected submission
* @return {obj} grade data
* { score, overallFeedback, criteria }
*/
export const selectedGradeData = createSelector(
[module.simpleSelectors.current],
(current) => current.gradeData,
);
/**
* Returns the response data for the selected submission
* @return {obj} response
* { text, files: [] }
*/
export const selectedResponse = createSelector(
[module.simpleSelectors.current],
(current) => current.response,
);
export const selected = StrictDict({
submissionId: module.selectedSubmissionId,
staticData: module.selectedStaticData,
username: module.selectedUsername,
gradeStatus: module.selectedGradeStatus,
gradeData: module.selectedGradeData,
response: module.selectedResponse,
});
/**
* Returns true iff there exists a selection previous to the current selection
* in the queue.
* @return {bool} has previous submission?
*/
export const hasPrevSubmission = createSelector(
[simpleSelectors.activeIndex],
(activeIndex) => activeIndex > 0,
);
/**
* Returns true iff there exists a selection after the current selection
* in the queue.
* @return {bool} has next submission?
*/
export const hasNextSubmission = createSelector(
[simpleSelectors.selected, simpleSelectors.activeIndex],
(list, activeIndex) => activeIndex < list.length - 1,
);
/**
* Returns the submissionId for the previous submission in the selection queu
* @return {string} previous submission id (null if there isn't one)
*/
export const prevSubmissionId = createSelector(
[simpleSelectors.selected, simpleSelectors.activeIndex],
(list, activeIndex) => {
if (activeIndex > 0) {
return list[activeIndex - 1];
}
return null;
},
);
/**
* Returns the submissionId for the next submission in the selection queu
* @return {string} next submission id (null if there isn't one)
*/
export const nextSubmissionId = createSelector(
[simpleSelectors.selected, simpleSelectors.activeIndex],
(list, activeIndex) => {
if (activeIndex < list.length - 1) {
return list[activeIndex + 1];
}
return null;
},
);
export default StrictDict({
...simpleSelectors,
listData,
selectedSubmissionId,
hasPrevSubmission,
hasNextSubmission,
nextSubmissionId,
prevSubmissionId,
selected,
});

View File

@@ -72,9 +72,9 @@ const fetchSubmission = mockSuccess((submissionId) => (
* },
* }
*/
const fetchSubmissionStatus = mockSuccess((submissionId) => ({
submissionData: fakeData.mockSubmission(submissionId),
}));
const fetchSubmissionStatus = mockSuccess((submissionId) => (
fakeData.mockSubmissionStatus(submissionId)
));
/**
* Fetches only the learner response for a given submission. Used for pre-fetching response

View File

@@ -1,5 +1,5 @@
import submissions from './submissionList';
import { mockSubmission } from './submissionFull';
import { mockSubmission, mockSubmissionStatus } from './submissionFull';
import oraMetadata from './ora';
import courseMetadata from './course';
import ids from './ids';
@@ -9,5 +9,6 @@ export default {
oraMetadata,
courseMetadata,
mockSubmission,
mockSubmissionStatus,
ids,
};

View File

@@ -15,7 +15,7 @@ const rubricConfig = {
comments: 'rubric-level comments',
criteria: [
{
name: 'First Criterion',
name: 'firstCriterion',
orderNum: 0,
prompt: 'A criterion prompt',
feedback: 'optional',

View File

@@ -1,21 +1,22 @@
import submissionList from './submissionList';
const responseText = `<div><h1>Title</h1>
const responseText = (submissionId) => `<div><h1>Title (${submissionId})</h1>
Phasellus tempor eros aliquam ipsum molestie, vitae varius lectus tempus. Morbi iaculis, libero euismod vehicula rutrum, nisi leo volutpat diam, quis commodo ex nunc ut odio. Pellentesque condimentum feugiat erat ac vulputate. Pellentesque porta rutrum sagittis. Curabitur vulputate tempus accumsan. Fusce bibendum gravida metus a scelerisque. Mauris fringilla orci non lobortis commodo. Quisque iaculis, quam a tincidunt vehicula, erat nisi accumsan quam, eu cursus ligula magna id odio. Nulla porttitor, lorem gravida vehicula tristique, sapien metus tristique ex, id tincidunt sapien justo nec sapien. Maecenas luctus, nisl vestibulum scelerisque pharetra, ligula orci vulputate turpis, in ultrices mauris dolor eu enim. Suspendisse quis nibh nec augue semper maximus. Morbi maximus eleifend magna.
Phasellus porttitor vel magna et auctor. Nulla porttitor convallis aliquam. Donec cursus, ipsum ut egestas bibendum, purus metus dignissim est, ac condimentum leo felis eget diam. In magna mi, tincidunt id sapien id, fermentum vestibulum quam. Quisque et dui sed urna convallis rutrum pellentesque quis sapien. Cras non lectus velit. Praesent semper eros id risus mollis, quis interdum quam imperdiet. Sed nec vulputate tortor, at tristique tortor.
</div>`;
// eslint-disable-next-line
export const mockSubmission = (submissionId) => {
console.log({ submissionId, submission: submissionList[submissionId] });
return {
response: {
text: responseText,
files: [],
},
gradeStatus: submissionList[submissionId].gradeStatus,
gradeData: submissionList[submissionId].grade,
};
};
export const mockSubmission = (submissionId) => ({
response: {
text: responseText(submissionId),
files: [],
},
gradeStatus: submissionList[submissionId].gradeStatus,
score: submissionList[submissionId].score,
});
export const mockSubmissionStatus = (submissionId) => ({
gradeData: submissionList[submissionId].gradeData,
gradeStatus: submissionList[submissionId].gradeStatus,
});

View File

@@ -22,17 +22,27 @@ const day = 86400000;
const submissions = {};
let lastIndex = 0;
const createSubmission = (grade, gradeStatus) => {
const createSubmission = (score, gradeStatus) => {
const index = lastIndex;
lastIndex += 1;
const submissionId = ids.submissionId(index);
const gradeData = score === null ? null : {
score,
overallFeedback: 'was okay',
criteria: [{
name: 'firstCriterion',
feedback: 'did alright',
selectedOption: 'good'
}],
};
submissions[submissionId] = {
submissionId,
username: ids.username(index),
// teamName: '',
dateSubmitted: date0 + (day * index),
score,
gradeData,
gradeStatus,
grade,
};
};

View File

@@ -3,6 +3,7 @@ import { StrictDict } from 'utils';
import actions from 'data/actions';
import selectors from 'data/selectors';
import api from 'data/services/lms/api';
import * as module from './grading';
/**
* Prefetch the "next" submission in the selected queue. Only fetches the response info.
@@ -11,7 +12,7 @@ export const prefetchNext = () => (dispatch, getState) => (
api.fetchSubmissionResponse(
selectors.grading.nextSubmissionId(getState()),
).then((response) => {
dispatch(actions.grading.preloadNext(response.submission));
dispatch(actions.grading.preloadNext(response));
})
);
@@ -22,7 +23,7 @@ export const prefetchPrev = () => (dispatch, getState) => (
api.fetchSubmissionResponse(
selectors.grading.prevSubmissionId(getState()),
).then((response) => {
dispatch(actions.grading.preloadPrev(response.submissionStatus));
dispatch(actions.grading.preloadPrev(response));
})
);