From 1ffc93dc6d9baaa0a1de81799432b46094c934c1 Mon Sep 17 00:00:00 2001 From: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com> Date: Mon, 6 Jan 2025 15:12:40 -0500 Subject: [PATCH] feat: update grade summary to show floating point grades (#1558) This PR resolves the bug that shows assignment's weighted grades that do not sum to the correct total grade. When a learner's weighted grades round down to the nearest whole number, but the summation of the weighted grades will round to a higher percent than the pre-rounded summation. To clarify this for users, the assignment's weighted grade will now show 2 decimal points, matching the legacy display found in the grade graph. To further clarify the difference, a tooltip was added to show the learner the raw weighted grade and the rounded weighted grade. --- .../grade-summary/GradeSummaryHeader.jsx | 31 +++------- .../grade-summary/GradeSummaryTable.jsx | 43 ++++++++----- .../grade-summary/GradeSummaryTableFooter.jsx | 61 +++++++++++++++---- .../progress-tab/grades/messages.ts | 6 +- 4 files changed, 91 insertions(+), 50 deletions(-) diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx index fc860c10fa..99eb6e82c1 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryHeader.jsx @@ -1,49 +1,39 @@ -import React, { useState } from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { - Icon, IconButton, OverlayTrigger, Popover, -} from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Icon, OverlayTrigger, Tooltip } from '@openedx/paragon'; import { Blocked, InfoOutline } from '@openedx/paragon/icons'; import messages from '../messages'; import { useModel } from '../../../../generic/model-store'; -const GradeSummaryHeader = ({ intl, allOfSomeAssignmentTypeIsLocked }) => { +const GradeSummaryHeader = ({ allOfSomeAssignmentTypeIsLocked }) => { + const intl = useIntl(); const { courseId, } = useSelector(state => state.courseHome); const { gradesFeatureIsFullyLocked, } = useModel('progress', courseId); - const [showTooltip, setShowTooltip] = useState(false); return (

{intl.formatMessage(messages.gradeSummary)}

- - {intl.formatMessage(messages.gradeSummaryTooltipBody)} - - + + {intl.formatMessage(messages.gradeSummaryTooltipBody)} + )} > - { setShowTooltip(!showTooltip); }} - onBlur={() => { setShowTooltip(false); }} + {!gradesFeatureIsFullyLocked && allOfSomeAssignmentTypeIsLocked && ( @@ -57,8 +47,7 @@ const GradeSummaryHeader = ({ intl, allOfSomeAssignmentTypeIsLocked }) => { }; GradeSummaryHeader.propTypes = { - intl: intlShape.isRequired, allOfSomeAssignmentTypeIsLocked: PropTypes.bool.isRequired, }; -export default injectIntl(GradeSummaryHeader); +export default GradeSummaryHeader; diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx index 628a65e24a..bd805242d0 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx @@ -1,10 +1,7 @@ -import React from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; -import { - getLocale, injectIntl, intlShape, isRtl, -} from '@edx/frontend-platform/i18n'; +import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n'; import { DataTable } from '@openedx/paragon'; import { useModel } from '../../../../generic/model-store'; @@ -14,7 +11,8 @@ import GradeSummaryTableFooter from './GradeSummaryTableFooter'; import messages from '../messages'; -const GradeSummaryTable = ({ intl, setAllOfSomeAssignmentTypeIsLocked }) => { +const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => { + const intl = useIntl(); const { courseId, } = useSelector(state => state.courseHome); @@ -34,6 +32,14 @@ const GradeSummaryTable = ({ intl, setAllOfSomeAssignmentTypeIsLocked }) => { return footnoteId.replace(/[^A-Za-z0-9.-_]+/g, '-'); }; + const getGradePercent = (grade) => { + if (Number.isInteger(grade * 100)) { + return (grade * 100).toFixed(0); + } + + return (grade * 100).toFixed(2); + }; + const hasNoAccessToAssignmentsOfType = (assignmentType) => { const subsectionAssignmentsOfType = sectionScores.map((chapter) => chapter.subsections.filter((subsection) => ( subsection.assignmentType === assignmentType && subsection.hasGradedAssignment @@ -52,31 +58,37 @@ const GradeSummaryTable = ({ intl, setAllOfSomeAssignmentTypeIsLocked }) => { }; const gradeSummaryData = assignmentPolicies.map((assignment) => { + const { + averageGrade, + numDroppable, + type: assignmentType, + weight, + weightedGrade, + } = assignment; let footnoteId = ''; let footnoteMarker; - if (assignment.numDroppable > 0) { + if (numDroppable > 0) { footnoteId = getFootnoteId(assignment); footnotes.push({ id: footnoteId, - numDroppable: assignment.numDroppable, - assignmentType: assignment.type, + numDroppable, + assignmentType, }); footnoteMarker = footnotes.length; } - const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignment.type); - + const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignmentType); const isLocaleRtl = isRtl(getLocale()); return { type: { - footnoteId, footnoteMarker, type: assignment.type, locked, + footnoteId, footnoteMarker, type: assignmentType, locked, }, - weight: { weight: `${(assignment.weight * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked }, - grade: { grade: `${(assignment.averageGrade * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked }, - weightedGrade: { weightedGrade: `${(assignment.weightedGrade * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked }, + weight: { weight: `${(weight * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked }, + grade: { grade: `${getGradePercent(averageGrade)}${isLocaleRtl ? '\u200f' : ''}%`, locked }, + weightedGrade: { weightedGrade: `${getGradePercent(weightedGrade)}${isLocaleRtl ? '\u200f' : ''}%`, locked }, }; }); const getAssignmentTypeCell = (value) => ( @@ -137,8 +149,7 @@ const GradeSummaryTable = ({ intl, setAllOfSomeAssignmentTypeIsLocked }) => { }; GradeSummaryTable.propTypes = { - intl: intlShape.isRequired, setAllOfSomeAssignmentTypeIsLocked: PropTypes.func.isRequired, }; -export default injectIntl(GradeSummaryTable); +export default GradeSummaryTable; diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTableFooter.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTableFooter.jsx index 2c3235be86..19299a4ef8 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTableFooter.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTableFooter.jsx @@ -1,15 +1,34 @@ -import React from 'react'; +import { useContext } from 'react'; import { useSelector } from 'react-redux'; +import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n'; import { - getLocale, injectIntl, intlShape, isRtl, -} from '@edx/frontend-platform/i18n'; -import { DataTable } from '@openedx/paragon'; -import { useModel } from '../../../../generic/model-store'; + DataTable, + DataTableContext, + Icon, + OverlayTrigger, + Stack, + Tooltip, +} from '@openedx/paragon'; +import { InfoOutline } from '@openedx/paragon/icons'; +import { useModel } from '../../../../generic/model-store'; import messages from '../messages'; -const GradeSummaryTableFooter = ({ intl }) => { +const GradeSummaryTableFooter = () => { + const intl = useIntl(); + + const { data } = useContext(DataTableContext); + + const rawGrade = data.reduce( + (grade, currentValue) => { + const { weightedGrade } = currentValue.weightedGrade; + const percent = weightedGrade.replace(/%/g, '').trim(); + return grade + parseFloat(percent); + }, + 0, + ).toFixed(2); + const { courseId, } = useSelector(state => state.courseHome); @@ -29,15 +48,33 @@ const GradeSummaryTableFooter = ({ intl }) => { return (
-
{intl.formatMessage(messages.weightedGradeSummary)}
+
+ + {intl.formatMessage(messages.weightedGradeSummary)} + + {intl.formatMessage( + messages.weightedGradeSummaryTooltip, + { roundedGrade: totalGrade, rawGrade }, + )} + + )} + > + + + +
{totalGrade}{isLocaleRtl && '\u200f'}%
); }; -GradeSummaryTableFooter.propTypes = { - intl: intlShape.isRequired, -}; - -export default injectIntl(GradeSummaryTableFooter); +export default GradeSummaryTableFooter; diff --git a/src/course-home/progress-tab/grades/messages.ts b/src/course-home/progress-tab/grades/messages.ts index fb57809852..24475b98fd 100644 --- a/src/course-home/progress-tab/grades/messages.ts +++ b/src/course-home/progress-tab/grades/messages.ts @@ -203,7 +203,11 @@ const messages = defineMessages({ defaultMessage: 'Your current weighted grade summary', description: 'It the text precede the sum of weighted grades of all the assignment', }, - + weightedGradeSummaryTooltip: { + id: 'progress.weightedGradeSummary', + defaultMessage: 'Your raw weighted grade summary is {rawGrade} and rounds to {roundedGrade}.', + description: 'Tooltip content that explains the rounding of the summary versus individual assignments', + }, }); export default messages;