diff --git a/app/jsx/outcomes/IndividualStudentMastery/__tests__/__snapshots__/AssignmentResult.test.js.snap b/app/jsx/outcomes/IndividualStudentMastery/__tests__/__snapshots__/AssignmentResult.test.js.snap index 1ba08784a56..038f9ff73be 100644 --- a/app/jsx/outcomes/IndividualStudentMastery/__tests__/__snapshots__/AssignmentResult.test.js.snap +++ b/app/jsx/outcomes/IndividualStudentMastery/__tests__/__snapshots__/AssignmentResult.test.js.snap @@ -28,7 +28,7 @@ exports[`renders the AlignmentResult component 1`] = `
- +
diff --git a/app/jsx/rubrics/Comments.js b/app/jsx/rubrics/Comments.js index 719c48df477..856a7efc485 100644 --- a/app/jsx/rubrics/Comments.js +++ b/app/jsx/rubrics/Comments.js @@ -104,7 +104,7 @@ CommentText.defaultProps = { } const Comments = (props) => { - const { assessing, assessment, ...commentProps } = props + const { assessing, assessment, footer, ...commentProps } = props if (!assessing || assessment === null) { return (
@@ -113,6 +113,7 @@ const Comments = (props) => { placeholder={I18n.t("This area will be used by the assessor to leave comments related to this criterion.")} weight="normal" /> + {footer}
) } @@ -129,9 +130,13 @@ const Comments = (props) => { Comments.propTypes = { assessing: PropTypes.bool.isRequired, assessment: PropTypes.shape(assessmentShape), + footer: PropTypes.node, savedComments: PropTypes.arrayOf(PropTypes.string).isRequired, setComments: PropTypes.func.isRequired, setSaveLater: PropTypes.func.isRequired } +Comments.defaultProps = { + footer: null +} export default Comments diff --git a/app/jsx/rubrics/Criterion.js b/app/jsx/rubrics/Criterion.js index eea00c11085..a50affdf66a 100644 --- a/app/jsx/rubrics/Criterion.js +++ b/app/jsx/rubrics/Criterion.js @@ -116,10 +116,11 @@ export default class Criterion extends React.Component { const { assessment, criterion, + customRatings, freeForm, onAssessmentChange, savedComments, - customRatings + isSummary } = this.props const { dialogOpen } = this.state const isOutcome = criterion.learning_outcome_id !== undefined @@ -135,24 +136,49 @@ export default class Criterion extends React.Component { } const onPointChange = assessing ? updatePoints : undefined + const pointsPossible = criterion.points + const pointsElement = () => ( + + ) + + const pointsComment = () => ( + + ) + + const pointsFooter = () => [ + pointsComment(), + pointsElement() + ] + const commentRating = ( onAssessmentChange({ saveCommentsForLater })} setComments={(comments) => onAssessmentChange({ comments })} /> ) + const ratings = freeForm ? commentRating : ( ) @@ -181,7 +207,6 @@ export default class Criterion extends React.Component { ) : null const noComments = (_.get(assessment, 'comments') || '').length > 0 - const pointsPossible = criterion.points const longDescription = criterion.long_description const threshold = criterion.mastery_points @@ -213,7 +238,7 @@ export default class Criterion extends React.Component { (freeForm || assessing || noComments) ? null : (
{I18n.t('Instructor Comments')} - + {pointsComment()}
) } @@ -221,30 +246,31 @@ export default class Criterion extends React.Component { {ratings} - - - {assessing && !freeForm ? commentInput : null} - + { + !isSummary ? ( + + {pointsElement()} + {assessing && !freeForm ? commentInput : null} + + ) : null + } ) } } Criterion.propTypes = { assessment: PropTypes.shape(assessmentShape), + customRatings: PropTypes.arrayOf(PropTypes.object), criterion: PropTypes.shape(criterionShape).isRequired, freeForm: PropTypes.bool.isRequired, onAssessmentChange: PropTypes.func, savedComments: PropTypes.arrayOf(PropTypes.string), - customRatings: PropTypes.arrayOf(PropTypes.object) + isSummary: PropTypes.bool } Criterion.defaultProps = { assessment: null, + customRatings: [], onAssessmentChange: null, savedComments: [], - customRatings: [] + isSummary: false } diff --git a/app/jsx/rubrics/Points.js b/app/jsx/rubrics/Points.js index 6c83a105fc0..a5bb7438ee2 100644 --- a/app/jsx/rubrics/Points.js +++ b/app/jsx/rubrics/Points.js @@ -27,7 +27,7 @@ import { assessmentShape } from './types' export const roundIfWhole = (n) => ( I18n.toNumber(n, { precision: Math.floor(n) === n ? 0 : 1 }) ) -const pointString = (n) => n !== null ? roundIfWhole(n) : '' +const pointString = (n) => n !== undefined ? roundIfWhole(n) : '--' export const possibleString = (possible) => I18n.t('%{possible} pts', { @@ -36,13 +36,13 @@ export const possibleString = (possible) => export const scoreString = (points, possible) => I18n.t('%{points} / %{possible}', { - points: roundIfWhole(points), + points: pointString(points), possible: possibleString(possible) }) const invalid = () => [{ text: I18n.t('Invalid value'), type: 'error' }] const messages = (points, pointsText) => - (points === null && pointsText) ? invalid() : undefined + (_.isNil(points) && pointsText) ? invalid() : undefined const Points = (props) => { const { @@ -68,6 +68,7 @@ const Points = (props) => { ) } else { + const usePointsText = pointsText !== null && pointsText !== undefined return (
{ label={{I18n.t('Points')}} messages={messages(points, pointsText)} onChange={(e) => onPointChange(e.target.value)} - value={pointsText || pointString(points)} + value={usePointsText ? pointsText : pointString(points)} width="4rem" /> {`/ ${possibleString(pointsPossible)}`}
diff --git a/app/jsx/rubrics/Ratings.js b/app/jsx/rubrics/Ratings.js index 94770c3b687..8fbac5edbb2 100644 --- a/app/jsx/rubrics/Ratings.js +++ b/app/jsx/rubrics/Ratings.js @@ -42,17 +42,27 @@ const pointString = (points, endOfRangePoints) => { export const Rating = (props) => { const { assessing, + classes, description, + endOfRangePoints, + footer, long_description, points, onClick, - endOfRangePoints, - classes, + isSummary, tierColor } = props - const shaderStyle = {backgroundColor: tierColor} - const triangleStyle = {borderBottomColor: tierColor} + const shaderStyle = { backgroundColor: tierColor } + const triangleStyle = { borderBottomColor: tierColor } + + const ratingPoints = () => ( +
+ + {pointString(points, endOfRangePoints)} + +
+ ) return (
{ role="button" tabIndex={assessing ? 0 : null} > -
- - {pointString(points, endOfRangePoints)} - -
+ {isSummary ? null : ratingPoints()}
{description} @@ -75,6 +81,13 @@ export const Rating = (props) => { {long_description} + { + footer !== null ? ( +
+ {footer} +
+ ) : null + }
@@ -105,9 +118,12 @@ const getCustomColor = (points, customRatings) => { Rating.propTypes = { ...tierShape, assessing: PropTypes.bool.isRequired, - selected: PropTypes.bool + footer: PropTypes.node, + selected: PropTypes.bool, + isSummary: PropTypes.bool.isRequired } Rating.defaultProps = { + footer: null, selected: false, endOfRangePoints: null // eslint-disable-line react/default-props-match-prop-types } @@ -115,12 +131,14 @@ Rating.defaultProps = { const Ratings = (props) => { const { assessing, + customRatings, + defaultMasteryThreshold, + footer, tiers, points, onPointChange, - defaultMasteryThreshold, - useRange, - customRatings + isSummary, + useRange } = props const pairs = tiers.map((tier, index) => { @@ -154,38 +172,55 @@ const Ratings = (props) => { } const selectedIndex = points !== undefined ? currentIndex() : null + const ratings = tiers.map((tier, index) => { + const selected = selectedIndex === index + if (isSummary && !selected) return null + const classes = classNames({ + 'rating-tier': true, + 'selected': selected, + }) + + return ( + onPointChange(tier.points)} + tierColor={getTierColor(selected)} + {...tier} + /> + ) + }).filter((v) => v !== null) + + const defaultRating = () => ( + + ) return (
- { - tiers.map((tier, index) => { - const selected = selectedIndex === index - const classes = classNames({ - 'rating-tier': true, - 'selected': selected, - }) - return ( - onPointChange(tier.points)} - classes={classes} - endOfRangePoints={useRange ? getRangePoints(tier.points, tiers[index + 1]) : null} - tierColor={getTierColor(selected)} - {...tier} - /> - ) - }) - } + {ratings.length > 0 || !isSummary ? ratings : defaultRating()}
) } Ratings.propTypes = { ...ratingShape, assessing: PropTypes.bool.isRequired, - onPointChange: PropTypes.func + footer: PropTypes.node, + onPointChange: PropTypes.func, + isSummary: PropTypes.bool.isRequired } Ratings.defaultProps = { + footer: null, onPointChange: () => { } } diff --git a/app/jsx/rubrics/Rubric.js b/app/jsx/rubrics/Rubric.js index 8c79398b09d..34a20757a8a 100644 --- a/app/jsx/rubrics/Rubric.js +++ b/app/jsx/rubrics/Rubric.js @@ -37,7 +37,15 @@ const totalAssessingString = (score, possible) => possible: I18n.toNumber(possible, { precision: 1 }) }) -const Rubric = ({ onAssessmentChange, rubric, rubricAssessment, rubricAssociation, customRatings }) => { +const Rubric = (props) => { + const { + customRatings, + onAssessmentChange, + rubric, + rubricAssessment, + rubricAssociation, + isSummary + } = props const assessing = onAssessmentChange !== null const priorData = _.get(rubricAssessment, 'data', []) const byCriteria = _.keyBy(priorData, (ra) => ra.criterion_id) @@ -59,10 +67,11 @@ const Rubric = ({ onAssessmentChange, rubric, rubricAssessment, rubricAssociatio key={criterion.id} assessment={assessment} criterion={criterion} + customRatings={customRatings} freeForm={rubric.free_form_criterion_comments} + isSummary={isSummary} onAssessmentChange={assessing ? onCriteriaChange(criterion.id) : undefined} savedComments={allComments[criterion.id]} - customRatings={customRatings} /> ) }) @@ -80,7 +89,11 @@ const Rubric = ({ onAssessmentChange, rubric, rubricAssessment, rubricAssociatio {I18n.t('Criteria')} {I18n.t('Ratings')} - {I18n.t('Pts')} + { + isSummary ? null : ( + {I18n.t('Pts')} + ) + } @@ -100,6 +113,7 @@ const Rubric = ({ onAssessmentChange, rubric, rubricAssessment, rubricAssociatio ) } Rubric.propTypes = { + customRatings: PropTypes.arrayOf(PropTypes.object), onAssessmentChange: PropTypes.func, rubric: PropTypes.shape(rubricShape).isRequired, rubricAssessment: (props) => { @@ -108,13 +122,14 @@ Rubric.propTypes = { return PropTypes.checkPropTypes({ rubricAssessment }, props, 'prop', 'Rubric') }, rubricAssociation: PropTypes.shape(rubricAssociationShape), - customRatings: PropTypes.arrayOf(PropTypes.object) + isSummary: PropTypes.bool } Rubric.defaultProps = { + customRatings: [], onAssessmentChange: null, rubricAssessment: null, rubricAssociation: {}, - customRatings: [] + isSummary: false } export default Rubric diff --git a/app/jsx/rubrics/__tests__/Comments.test.js b/app/jsx/rubrics/__tests__/Comments.test.js index e44672ca1f6..b3ff5ca54f6 100644 --- a/app/jsx/rubrics/__tests__/Comments.test.js +++ b/app/jsx/rubrics/__tests__/Comments.test.js @@ -74,4 +74,10 @@ describe('The Comments component', () => { expect(setSaveLater.args).toEqual([[true]]) }) + + it('renders a footer after the comment when provided', () => { + const el = component({ assessing: false, footer:
this is a footer
}) + + expect(el.shallow().debug()).toMatchSnapshot() + }) }) diff --git a/app/jsx/rubrics/__tests__/Criterion.test.js b/app/jsx/rubrics/__tests__/Criterion.test.js index a715cf612bb..d2b5478cb28 100644 --- a/app/jsx/rubrics/__tests__/Criterion.test.js +++ b/app/jsx/rubrics/__tests__/Criterion.test.js @@ -95,4 +95,17 @@ describe('Criterion', () => { dialog.prop('close')() expectState(false) }) + + it('does not have a points column in summary mode', () => { + const el = shallow( + + ) + + expect(el.find('td')).toHaveLength(1) + }) }) diff --git a/app/jsx/rubrics/__tests__/Points.test.js b/app/jsx/rubrics/__tests__/Points.test.js index cd4081724ef..8895d72a00d 100644 --- a/app/jsx/rubrics/__tests__/Points.test.js +++ b/app/jsx/rubrics/__tests__/Points.test.js @@ -40,7 +40,7 @@ describe('The Points component', () => { it('renders blank when points are undefined', () => { expect(component({ assessing: true, - assessment: id, + assessment: { ...id, pointsText: '' }, pointsPossible: 2 }).debug()).toMatchSnapshot() }) @@ -57,7 +57,7 @@ describe('The Points component', () => { assessing: true, assessment: { ...id, - points: points === undefined ? null : points, + points, pointsText, }, pointsPossible: 2 diff --git a/app/jsx/rubrics/__tests__/Ratings.test.js b/app/jsx/rubrics/__tests__/Ratings.test.js index 8b5c6c16462..5d6b4cfe254 100644 --- a/app/jsx/rubrics/__tests__/Ratings.test.js +++ b/app/jsx/rubrics/__tests__/Ratings.test.js @@ -23,13 +23,15 @@ import Ratings, { Rating } from '../Ratings' describe('The Ratings component', () => { const props = { assessing: false, + footer: null, tiers: [ { description: 'Superb', points: 10 }, { description: 'Meh', long_description: 'More Verbosity', points: 5 }, { description: 'Subpar', points: 1 } ], - points: 5, defaultMasteryThreshold: 10, + points: 5, + isSummary: false, useRange: false } @@ -89,23 +91,41 @@ describe('The Ratings component', () => { expect(ratings(0, true)).toEqual(['transparent', 'transparent', '#F8971C']) }) - describe('Rating component', () => { - it('is navigable and clickable when assessing', () => { - const onClick = sinon.spy() - const wrapper = shallow() - const div = wrapper.find('div').at(0) - expect(div.prop('tabIndex')).toEqual(0) - div.simulate('click') - expect(onClick.called).toBe(true) - }) + it('is navigable and clickable when assessing', () => { + const onClick = sinon.spy() + const wrapper = shallow() + const div = wrapper.find('div').at(0) + expect(div.prop('tabIndex')).toEqual(0) + div.simulate('click') + expect(onClick.called).toBe(true) + }) - it('is not navigable or clickable when not assessing', () => { - const onClick = sinon.spy() - const wrapper = shallow() - const div = wrapper.find('div').at(0) - expect(div.prop('tabIndex')).toBeNull() - div.simulate('click') - expect(onClick.called).toBe(false) - }) + it('is not navigable or clickable when not assessing', () => { + const onClick = sinon.spy() + const wrapper = shallow() + const div = wrapper.find('div').at(0) + expect(div.prop('tabIndex')).toBeNull() + div.simulate('click') + expect(onClick.called).toBe(false) + }) + + it('only renders the single selected Rating with a footer in summary mode', () => { + const el = component({ points: 5, isSummary: true, footer:
ow my foot
}) + const ratings = el.find('Rating') + + expect(ratings).toHaveLength(1) + + const rating = ratings.at(0) + expect(rating.shallow().debug()).toMatchSnapshot() + }) + + it('renders a default rating if none of the ratings are selected', () => { + const el = component({ points: 6, isSummary: true, footer:
ow my foot
}) + const ratings = el.find('Rating') + + expect(ratings).toHaveLength(1) + + const rating = ratings.at(0) + expect(rating.shallow().debug()).toMatchSnapshot() }) }) diff --git a/app/jsx/rubrics/__tests__/Rubric.test.js b/app/jsx/rubrics/__tests__/Rubric.test.js index e8de572a0be..2efed98dc42 100644 --- a/app/jsx/rubrics/__tests__/Rubric.test.js +++ b/app/jsx/rubrics/__tests__/Rubric.test.js @@ -82,4 +82,17 @@ describe('the Rubric component', () => { expect(renderAssessing(onAssessmentChange.args[0][0]).debug()).toMatchSnapshot() }) + + it('does not have a points column in summary mode', () => { + const el = shallow( + + ) + + expect(el.find('th')).toHaveLength(2) + }) }) diff --git a/app/jsx/rubrics/__tests__/__snapshots__/Comments.test.js.snap b/app/jsx/rubrics/__tests__/__snapshots__/Comments.test.js.snap index 4a9b33222ea..285d6da466c 100644 --- a/app/jsx/rubrics/__tests__/__snapshots__/Comments.test.js.snap +++ b/app/jsx/rubrics/__tests__/__snapshots__/Comments.test.js.snap @@ -2,6 +2,15 @@ exports[`The Comments component directly renders comments_html 1`] = `"
I award you no points, and may God have mercy on your soul.
"`; +exports[`The Comments component renders a footer after the comment when provided 1`] = ` +"
+ +
+ this is a footer +
+
" +`; + exports[`The Comments component renders a placeholder when no assessment provided 1`] = ` " This area will be used by the assessor to leave comments related to this criterion. diff --git a/app/jsx/rubrics/__tests__/__snapshots__/Criterion.test.js.snap b/app/jsx/rubrics/__tests__/__snapshots__/Criterion.test.js.snap index b5a13c678c7..707308ccb9b 100644 --- a/app/jsx/rubrics/__tests__/__snapshots__/Criterion.test.js.snap +++ b/app/jsx/rubrics/__tests__/__snapshots__/Criterion.test.js.snap @@ -33,7 +33,7 @@ exports[`Free-form Rubric with a custom criterion by default renders the root co
- + @@ -72,7 +72,7 @@ exports[`Free-form Rubric with a custom criterion when assessing renders the roo
- + @@ -111,7 +111,7 @@ exports[`Free-form Rubric with a custom criterion without an assessment renders
- + @@ -177,7 +177,7 @@ exports[`Free-form Rubric with a outcome criterion by default renders the root c - + @@ -243,7 +243,7 @@ exports[`Free-form Rubric with a outcome criterion when assessing renders the ro - + @@ -309,7 +309,7 @@ exports[`Free-form Rubric with a outcome criterion without an assessment renders - + @@ -348,7 +348,7 @@ exports[`Point Rubric with a custom criterion by default renders the root compon - + @@ -387,7 +387,7 @@ exports[`Point Rubric with a custom criterion when assessing renders the root co - + @@ -433,7 +433,7 @@ exports[`Point Rubric with a custom criterion without an assessment renders the - + @@ -499,7 +499,7 @@ exports[`Point Rubric with a outcome criterion by default renders the root compo - + @@ -565,7 +565,7 @@ exports[`Point Rubric with a outcome criterion when assessing renders the root c - + @@ -638,7 +638,7 @@ exports[`Point Rubric with a outcome criterion without an assessment renders the - + diff --git a/app/jsx/rubrics/__tests__/__snapshots__/Ratings.test.js.snap b/app/jsx/rubrics/__tests__/__snapshots__/Ratings.test.js.snap index fe1ce59ca4d..d6bd7a7c5eb 100644 --- a/app/jsx/rubrics/__tests__/__snapshots__/Ratings.test.js.snap +++ b/app/jsx/rubrics/__tests__/__snapshots__/Ratings.test.js.snap @@ -1,5 +1,45 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`The Ratings component only renders the single selected Rating with a footer in summary mode 1`] = ` +"
+
+ + Meh + +
+ + More Verbosity + +
+
+ ow my foot +
+
+
+
+
+
" +`; + +exports[`The Ratings component renders a default rating if none of the ratings are selected 1`] = ` +"
+
+ + No details + +
+ +
+
+ ow my foot +
+
+
+
+
+
" +`; + exports[`The Ratings component renders the Rating sub-components as expected when range rating enabled 1`] = ` "
@@ -61,8 +101,8 @@ exports[`The Ratings component renders the Rating sub-components as expected whe exports[`The Ratings component renders the root component as expected 1`] = ` "
- - - + + +
" `; diff --git a/app/jsx/rubrics/__tests__/__snapshots__/Rubric.test.js.snap b/app/jsx/rubrics/__tests__/__snapshots__/Rubric.test.js.snap index 1eda4dc6a22..620c2d3c794 100644 --- a/app/jsx/rubrics/__tests__/__snapshots__/Rubric.test.js.snap +++ b/app/jsx/rubrics/__tests__/__snapshots__/Rubric.test.js.snap @@ -17,8 +17,8 @@ exports[`the Rubric component hides the score total when needed 1`] = ` - - + + @@ -48,8 +48,8 @@ exports[`the Rubric component renders as expected 1`] = ` - - + + @@ -81,8 +81,8 @@ exports[`the Rubric component renders properly with no assessment 1`] = ` - - + + @@ -114,8 +114,8 @@ exports[`the Rubric component updates the total score when an individual criteri - - + + diff --git a/app/stylesheets/components/_rubric.scss b/app/stylesheets/components/_rubric.scss index 45c4c922a9c..5415d042ccf 100644 --- a/app/stylesheets/components/_rubric.scss +++ b/app/stylesheets/components/_rubric.scss @@ -109,6 +109,10 @@ position: relative; } +.react-rubric .rating-tier .rating-footer { + padding-bottom: 1rem; +} + .react-rubric .rating-tier.selected { .shader { width: 100%; @@ -137,4 +141,5 @@ .react-rubric .graded-points { white-space: nowrap; + text-align: end; } diff --git a/public/javascripts/rubric_assessment.js b/public/javascripts/rubric_assessment.js index 8cf73ae183a..0ed5d29b4eb 100644 --- a/public/javascripts/rubric_assessment.js +++ b/public/javascripts/rubric_assessment.js @@ -276,11 +276,16 @@ window.rubricAssessment = { }, fillAssessment: function(rubric, partialAssessment) { + const fillText = (c) => ({ + pointsText: _.isNil(c.points) && _.isUndefined(c.pointsText) ? '--' : c.pointsText, + ...c + }) + const defaultCriteria = (id) => ({ criterion_id: id, pointsText: '' }) const prior = _.keyBy(_.cloneDeep(partialAssessment.data), (c) => c.criterion_id) return { score: 0, ...partialAssessment, - data: rubric.criteria.map((c) => (prior[c.id] || { criterion_id: c.id, score: null })) + data: rubric.criteria.map((c) => fillText(prior[c.id] || defaultCriteria(c.id))) } }, @@ -374,6 +379,29 @@ window.rubricAssessment = { } }, + populateNewRubricSummary: function(container, assessment, rubricAssociation, editData) { + if (ENV.nonScoringRubrics && ENV.rubric) { + if(assessment) { + const filled = rubricAssessment.fillAssessment(ENV.rubric, assessment || {}) + ReactDOM.render(React.createElement(Rubric, { + customRatings: ENV.outcome_proficiency ? ENV.outcome_proficiency.ratings : [], + rubric: ENV.rubric, + rubricAssessment: filled, + rubricAssociation, + isSummary: true + }, null), container.get(0)) + } else { + container.get(0).innerHTML = '' + } + } else { + rubricAssessment.populateRubricSummary( + container, + assessment, + editData + ) + } + }, + populateRubricSummary: function($rubricSummary, data, editing_data) { $rubricSummary.find(".criterion_points").text("").end() .find(".rating_custom").text(""); diff --git a/public/javascripts/speed_grader.js b/public/javascripts/speed_grader.js index 3eac0c31b6d..8856cdbff12 100644 --- a/public/javascripts/speed_grader.js +++ b/public/javascripts/speed_grader.js @@ -837,9 +837,10 @@ function initRubricStuff(){ selectors.get('#rubric_assessments_select').change(() => { const editingData = rubricAssessment.assessmentData($("#rubric_full")) var selectedAssessment = getSelectedAssessment(); - rubricAssessment.populateRubricSummary( + rubricAssessment.populateNewRubricSummary( $("#rubric_summary_holder .rubric_summary"), selectedAssessment, + jsonData.rubric_association, editingData ); });