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;