Skip to content

Commit

Permalink
feat: update grade summary to show floating point grades (#1558)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
KristinAoki authored Jan 6, 2025
1 parent 346e15a commit 1ffc93d
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -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 (
<div className="row w-100 m-0 align-items-center">
<h3 className="h4 mb-3 mr-1">{intl.formatMessage(messages.gradeSummary)}</h3>
<OverlayTrigger
trigger="click"
trigger="hover"
placement="top"
show={showTooltip}
overlay={(
<Popover>
<Popover.Content className="small text-dark-700">
{intl.formatMessage(messages.gradeSummaryTooltipBody)}
</Popover.Content>
</Popover>
<Tooltip>
{intl.formatMessage(messages.gradeSummaryTooltipBody)}
</Tooltip>
)}
>
<IconButton
onClick={() => { setShowTooltip(!showTooltip); }}
onBlur={() => { setShowTooltip(false); }}
<Icon
alt={intl.formatMessage(messages.gradeSummaryTooltipAlt)}
src={InfoOutline}
iconAs={Icon}
className="mb-3"
size="sm"
disabled={gradesFeatureIsFullyLocked}
/>
</OverlayTrigger>
{!gradesFeatureIsFullyLocked && allOfSomeAssignmentTypeIsLocked && (
Expand All @@ -57,8 +47,7 @@ const GradeSummaryHeader = ({ intl, allOfSomeAssignmentTypeIsLocked }) => {
};

GradeSummaryHeader.propTypes = {
intl: intlShape.isRequired,
allOfSomeAssignmentTypeIsLocked: PropTypes.bool.isRequired,
};

export default injectIntl(GradeSummaryHeader);
export default GradeSummaryHeader;
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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) => (
Expand Down Expand Up @@ -137,8 +149,7 @@ const GradeSummaryTable = ({ intl, setAllOfSomeAssignmentTypeIsLocked }) => {
};

GradeSummaryTable.propTypes = {
intl: intlShape.isRequired,
setAllOfSomeAssignmentTypeIsLocked: PropTypes.func.isRequired,
};

export default injectIntl(GradeSummaryTable);
export default GradeSummaryTable;
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -29,15 +48,33 @@ const GradeSummaryTableFooter = ({ intl }) => {
return (
<DataTable.TableFooter className={`border-top border-primary ${bgColor}`}>
<div className="row w-100 m-0">
<div id="weighted-grade-summary" className="col-8 p-0 small">{intl.formatMessage(messages.weightedGradeSummary)}</div>
<div id="weighted-grade-summary" className="col-8 p-0 small">
<Stack gap={2} direction="horizontal">
{intl.formatMessage(messages.weightedGradeSummary)}
<OverlayTrigger
trigger="hover"
placement="bottom"
overlay={(
<Tooltip>
{intl.formatMessage(
messages.weightedGradeSummaryTooltip,
{ roundedGrade: totalGrade, rawGrade },
)}
</Tooltip>
)}
>
<Icon
src={InfoOutline}
size="sm"
alt={intl.formatMessage(messages.gradeSummaryTooltipAlt)}
/>
</OverlayTrigger>
</Stack>
</div>
<div data-testid="gradeSummaryFooterTotalWeightedGrade" aria-labelledby="weighted-grade-summary" className="col-4 p-0 text-right font-weight-bold small">{totalGrade}{isLocaleRtl && '\u200f'}%</div>
</div>
</DataTable.TableFooter>
);
};

GradeSummaryTableFooter.propTypes = {
intl: intlShape.isRequired,
};

export default injectIntl(GradeSummaryTableFooter);
export default GradeSummaryTableFooter;
6 changes: 5 additions & 1 deletion src/course-home/progress-tab/grades/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

0 comments on commit 1ffc93d

Please sign in to comment.