From 52692dc6620bde88761a0d4e5d746f27b9c4c9c7 Mon Sep 17 00:00:00 2001 From: Muhammad Anas <88967643+Anas12091101@users.noreply.github.com> Date: Fri, 24 Oct 2025 22:55:12 +0500 Subject: [PATCH] refactor: shift grade summary calculation to backend and display "hidden grades" label in the grade table (#1797) Refactors the grade summary logic to delegate all calculation responsibilities to the backend. Previously, the frontend was performing grade summary computations using data fetched from the API. Now, the API itself provides the fully computed grade summary, simplifying the frontend and ensuring consistent results across clients. Additionally, a "Hidden Grades" label has been added in the grade summary table to clearly indicate sections where grades are not visible to learners. Finally, for visibility settings that depend on the due date, this PR adds a banner on the Progress page indicating that grades are not yet released, along with the relevant due date information. --- .../__factories__/progressTabData.factory.js | 14 ++ src/course-home/data/api.js | 92 ------- .../progress-tab/ProgressTab.test.jsx | 224 +++++++++--------- .../grades/course-grade/CourseGradeFooter.jsx | 126 +++++++--- .../course-grade/CurrentGradeTooltip.jsx | 12 + .../grades/grade-summary/GradeSummary.jsx | 6 +- .../grade-summary/GradeSummaryTable.jsx | 32 ++- .../grade-summary/GradeSummaryTableFooter.jsx | 23 +- .../progress-tab/grades/messages.ts | 20 ++ src/course-home/progress-tab/utils.ts | 12 + 10 files changed, 290 insertions(+), 271 deletions(-) diff --git a/src/course-home/data/__factories__/progressTabData.factory.js b/src/course-home/data/__factories__/progressTabData.factory.js index 1ff83241..3a3508f9 100644 --- a/src/course-home/data/__factories__/progressTabData.factory.js +++ b/src/course-home/data/__factories__/progressTabData.factory.js @@ -17,7 +17,21 @@ Factory.define('progressTabData') percent: 1, is_passing: true, }, + final_grades: 0.5, credit_course_requirements: null, + assignment_type_grade_summary: [ + { + type: 'Homework', + short_label: 'HW', + weight: 1, + average_grade: 1, + weighted_grade: 1, + num_droppable: 1, + num_total: 2, + has_hidden_contribution: 'none', + last_grade_publish_date: null, + }, + ], section_scores: [ { display_name: 'First section', diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 88d684c8..8254d4ef 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -3,93 +3,6 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { logInfo } from '@edx/frontend-platform/logging'; import { appendBrowserTimezoneToUrl } from '../../utils'; -const calculateAssignmentTypeGrades = (points, assignmentWeight, numDroppable) => { - let dropCount = numDroppable; - // Drop the lowest grades - while (dropCount && points.length >= dropCount) { - const lowestScore = Math.min(...points); - const lowestScoreIndex = points.indexOf(lowestScore); - points.splice(lowestScoreIndex, 1); - dropCount--; - } - let averageGrade = 0; - let weightedGrade = 0; - if (points.length) { - // Calculate the average grade for the assignment and round it. This rounding is not ideal and does not accurately - // reflect what a learner's grade would be, however, we must have parity with the current grading behavior that - // exists in edx-platform. - averageGrade = (points.reduce((a, b) => a + b, 0) / points.length).toFixed(4); - weightedGrade = averageGrade * assignmentWeight; - } - return { averageGrade, weightedGrade }; -}; - -function normalizeAssignmentPolicies(assignmentPolicies, sectionScores) { - const gradeByAssignmentType = {}; - assignmentPolicies.forEach(assignment => { - // Create an array with the number of total assignments and set the scores to 0 - // as placeholders for assignments that have not yet been released - gradeByAssignmentType[assignment.type] = { - grades: Array(assignment.numTotal).fill(0), - numAssignmentsCreated: 0, - numTotalExpectedAssignments: assignment.numTotal, - }; - }); - - sectionScores.forEach((chapter) => { - chapter.subsections.forEach((subsection) => { - if (!(subsection.hasGradedAssignment && subsection.showGrades && subsection.numPointsPossible)) { - return; - } - const { - assignmentType, - numPointsEarned, - numPointsPossible, - } = subsection; - - // If a subsection's assignment type does not match an assignment policy in Studio, - // we won't be able to include it in this accumulation of grades by assignment type. - // This may happen if a course author has removed/renamed an assignment policy in Studio and - // neglected to update the subsection's of that assignment type - if (!gradeByAssignmentType[assignmentType]) { - return; - } - - let { - numAssignmentsCreated, - } = gradeByAssignmentType[assignmentType]; - - numAssignmentsCreated++; - if (numAssignmentsCreated <= gradeByAssignmentType[assignmentType].numTotalExpectedAssignments) { - // Remove a placeholder grade so long as the number of recorded created assignments is less than the number - // of expected assignments - gradeByAssignmentType[assignmentType].grades.shift(); - } - // Add the graded assignment to the list - gradeByAssignmentType[assignmentType].grades.push(numPointsEarned ? numPointsEarned / numPointsPossible : 0); - // Record the created assignment - gradeByAssignmentType[assignmentType].numAssignmentsCreated = numAssignmentsCreated; - }); - }); - - return assignmentPolicies.map((assignment) => { - const { averageGrade, weightedGrade } = calculateAssignmentTypeGrades( - gradeByAssignmentType[assignment.type].grades, - assignment.weight, - assignment.numDroppable, - ); - - return { - averageGrade, - numDroppable: assignment.numDroppable, - shortLabel: assignment.shortLabel, - type: assignment.type, - weight: assignment.weight, - weightedGrade, - }; - }); -} - /** * Tweak the metadata for consistency * @param metadata the data to normalize @@ -236,11 +149,6 @@ export async function getProgressTabData(courseId, targetUserId) { const { data } = await getAuthenticatedHttpClient().get(url); const camelCasedData = camelCaseObject(data); - camelCasedData.gradingPolicy.assignmentPolicies = normalizeAssignmentPolicies( - camelCasedData.gradingPolicy.assignmentPolicies, - camelCasedData.sectionScores, - ); - // We replace gradingPolicy.gradeRange with the original data to preserve the intended casing for the grade. // For example, if a grade range key is "A", we do not want it to be camel cased (i.e. "A" would become "a") // in order to preserve a course team's desired grade formatting. diff --git a/src/course-home/progress-tab/ProgressTab.test.jsx b/src/course-home/progress-tab/ProgressTab.test.jsx index a08bf8a4..be99cab1 100644 --- a/src/course-home/progress-tab/ProgressTab.test.jsx +++ b/src/course-home/progress-tab/ProgressTab.test.jsx @@ -661,143 +661,133 @@ describe('Progress Tab', () => { expect(screen.getByText('Grade summary')).toBeInTheDocument(); }); - it('does not render Grade Summary when assignment policies are not populated', async () => { + it('does not render Grade Summary when assignment type grade summary is not populated', async () => { setTabData({ - grading_policy: { - assignment_policies: [], - grade_range: { - pass: 0.75, - }, - }, - section_scores: [], + assignment_type_grade_summary: [], }); await fetchAndRender(); expect(screen.queryByText('Grade summary')).not.toBeInTheDocument(); }); - it('calculates grades correctly when number of droppable assignments equals total number of assignments', async () => { + it('shows lock icon when all subsections of assignment type are hidden', async () => { setTabData({ grading_policy: { assignment_policies: [ - { - num_droppable: 2, - num_total: 2, - short_label: 'HW', - type: 'Homework', - weight: 1, - }, - ], - grade_range: { - pass: 0.75, - }, - }, - }); - await fetchAndRender(); - expect(screen.getByText('Grade summary')).toBeInTheDocument(); - // The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}" - expect(screen.getByRole('row', { name: 'Homework 1 100% 0% 0%' })).toBeInTheDocument(); - }); - it('calculates grades correctly when number of droppable assignments is less than total number of assignments', async () => { - await fetchAndRender(); - expect(screen.getByText('Grade summary')).toBeInTheDocument(); - // The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}" - expect(screen.getByRole('row', { name: 'Homework 1 100% 100% 100%' })).toBeInTheDocument(); - }); - it('calculates grades correctly when number of droppable assignments is zero', async () => { - setTabData({ - grading_policy: { - assignment_policies: [ - { - num_droppable: 0, - num_total: 2, - short_label: 'HW', - type: 'Homework', - weight: 1, - }, - ], - grade_range: { - pass: 0.75, - }, - }, - }); - await fetchAndRender(); - expect(screen.getByText('Grade summary')).toBeInTheDocument(); - // The row is comprised of "{Assignment type} {weight} {grade} {weighted grade}" - expect(screen.getByRole('row', { name: 'Homework 100% 50% 50%' })).toBeInTheDocument(); - }); - it('calculates grades correctly when number of total assignments is less than the number of assignments created', async () => { - setTabData({ - grading_policy: { - assignment_policies: [ - { - num_droppable: 1, - num_total: 1, // two assignments created in the factory, but 1 is expected per Studio settings - short_label: 'HW', - type: 'Homework', - weight: 1, - }, - ], - grade_range: { - pass: 0.75, - }, - }, - }); - await fetchAndRender(); - expect(screen.getByText('Grade summary')).toBeInTheDocument(); - // The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}" - expect(screen.getByRole('row', { name: 'Homework 1 100% 100% 100%' })).toBeInTheDocument(); - }); - it('calculates grades correctly when number of total assignments is greater than the number of assignments created', async () => { - setTabData({ - grading_policy: { - assignment_policies: [ - { - num_droppable: 0, - num_total: 5, // two assignments created in the factory, but 5 are expected per Studio settings - short_label: 'HW', - type: 'Homework', - weight: 1, - }, - ], - grade_range: { - pass: 0.75, - }, - }, - }); - await fetchAndRender(); - expect(screen.getByText('Grade summary')).toBeInTheDocument(); - // The row is comprised of "{Assignment type} {weight} {grade} {weighted grade}" - expect(screen.getByRole('row', { name: 'Homework 100% 20% 20%' })).toBeInTheDocument(); - }); - it('calculates weighted grades correctly', async () => { - setTabData({ - grading_policy: { - assignment_policies: [ - { - num_droppable: 1, - num_total: 2, - short_label: 'HW', - type: 'Homework', - weight: 0.5, - }, { num_droppable: 0, num_total: 1, - short_label: 'Ex', - type: 'Exam', - weight: 0.5, + short_label: 'Final', + type: 'Final Exam', + weight: 1, }, ], grade_range: { pass: 0.75, }, }, + assignment_type_grade_summary: [ + { + type: 'Final Exam', + weight: 0.4, + average_grade: 0.0, + weighted_grade: 0.0, + last_grade_publish_date: '2025-10-15T14:17:04.368903Z', + has_hidden_contribution: 'all', + short_label: 'Final', + num_droppable: 0, + }, + ], }); await fetchAndRender(); - expect(screen.getByText('Grade summary')).toBeInTheDocument(); - // The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}" - expect(screen.getByRole('row', { name: 'Homework 1 50% 100% 50%' })).toBeInTheDocument(); - expect(screen.getByRole('row', { name: 'Exam 50% 0% 0%' })).toBeInTheDocument(); + // Should show lock icon for grade and weighted grade + expect(screen.getAllByTestId('lock-icon')).toHaveLength(2); + }); + + it('shows percent plus hidden grades when some subsections of assignment type are hidden', async () => { + setTabData({ + grading_policy: { + assignment_policies: [ + { + num_droppable: 0, + num_total: 2, + short_label: 'HW', + type: 'Homework', + weight: 1, + }, + ], + grade_range: { + pass: 0.75, + }, + }, + assignment_type_grade_summary: [ + { + type: 'Homework', + weight: 1, + average_grade: 0.25, + weighted_grade: 0.25, + last_grade_publish_date: '2025-10-15T14:17:04.368903Z', + has_hidden_contribution: 'some', + short_label: 'HW', + num_droppable: 0, + }, + ], + }); + await fetchAndRender(); + // Should show percent + hidden scores for grade and weighted grade + const hiddenScoresCells = screen.getAllByText(/% \+ Hidden Scores/); + expect(hiddenScoresCells).toHaveLength(2); + // Only correct visible scores should be shown (from subsection2) + // The correct visible score is 1/4 = 0.25 -> 25% + expect(hiddenScoresCells[0]).toHaveTextContent('25% + Hidden Scores'); + expect(hiddenScoresCells[1]).toHaveTextContent('25% + Hidden Scores'); + }); + + it('displays a warning message with the latest due date when not all assignment scores are included in the total grade', async () => { + setTabData({ + grading_policy: { + assignment_policies: [ + { + num_droppable: 0, + num_total: 2, + short_label: 'HW', + type: 'Homework', + weight: 1, + }, + ], + grade_range: { + pass: 0.75, + }, + }, + assignment_type_grade_summary: [ + { + type: 'Homework', + weight: 1, + average_grade: 1, + weighted_grade: 1, + last_grade_publish_date: tomorrow.toISOString(), + has_hidden_contribution: 'none', + short_label: 'HW', + num_droppable: 0, + }, + ], + }); + + await fetchAndRender(); + + const formattedDateTime = new Intl.DateTimeFormat('en', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + timeZoneName: 'short', + }).format(tomorrow); + + expect( + screen.getByText( + `Some assignment scores are not yet included in your total grade. These grades will be released by ${formattedDateTime}.`, + ), + ).toBeInTheDocument(); }); it('renders override notice', async () => { diff --git a/src/course-home/progress-tab/grades/course-grade/CourseGradeFooter.jsx b/src/course-home/progress-tab/grades/course-grade/CourseGradeFooter.jsx index e075411f..6cada4cb 100644 --- a/src/course-home/progress-tab/grades/course-grade/CourseGradeFooter.jsx +++ b/src/course-home/progress-tab/grades/course-grade/CourseGradeFooter.jsx @@ -8,26 +8,57 @@ import { useModel } from '../../../../generic/model-store'; import GradeRangeTooltip from './GradeRangeTooltip'; import messages from '../messages'; +import { getLatestDueDateInFuture } from '../../utils'; + +const ResponsiveText = ({ + wideScreen, children, hasLetterGrades, passingGrade, +}) => { + const className = wideScreen ? 'h4 m-0 align-bottom' : 'h5 align-bottom'; + const iconSize = wideScreen ? 'h3' : 'h4'; + + return ( + + {children} + {hasLetterGrades && ( + +   + + + )} + + ); +}; + +const NoticeRow = ({ + wideScreen, icon, bgClass, message, +}) => { + const textClass = wideScreen ? 'h4 m-0 align-bottom' : 'h5 align-bottom'; + return ( +
+
{icon}
+
+ {message} +
+
+ ); +}; const CourseGradeFooter = ({ passingGrade }) => { const intl = useIntl(); const courseId = useContextId(); const { - courseGrade: { - isPassing, - letterGrade, - }, - gradingPolicy: { - gradeRange, - }, + assignmentTypeGradeSummary, + courseGrade: { isPassing, letterGrade }, + gradingPolicy: { gradeRange }, } = useModel('progress', courseId); + const latestDueDate = getLatestDueDateInFuture(assignmentTypeGradeSummary); const wideScreen = useWindowSize().width >= breakpoints.medium.minWidth; + const hasLetterGrades = Object.keys(gradeRange).length > 1; - const hasLetterGrades = Object.keys(gradeRange).length > 1; // A pass/fail course will only have one key + // build footer text let footerText = intl.formatMessage(messages.courseGradeFooterNonPassing, { passingGrade }); - if (isPassing) { if (hasLetterGrades) { const minGradeRangeCutoff = gradeRange[letterGrade] * 100; @@ -47,42 +78,63 @@ const CourseGradeFooter = ({ passingGrade }) => { } } - const icon = isPassing ? - : ; + const passingIcon = isPassing ? ( + + ) : ( + + ); return ( -
-
- {icon} -
-
- {!wideScreen && ( - +
+ {footerText} - {hasLetterGrades && ( - -   - - - )} - + )} - {wideScreen && ( - - {footerText} - {hasLetterGrades && ( - -   - - - )} - - )} -
+ /> + {latestDueDate && ( + } + bgClass="bg-warning-100" + message={intl.formatMessage(messages.courseGradeFooterDueDateNotice, { + dueDate: intl.formatDate(latestDueDate, { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + timeZoneName: 'short', + }), + })} + /> + )}
); }; +ResponsiveText.propTypes = { + wideScreen: PropTypes.bool.isRequired, + children: PropTypes.node.isRequired, + hasLetterGrades: PropTypes.bool.isRequired, + passingGrade: PropTypes.number.isRequired, +}; + +NoticeRow.propTypes = { + wideScreen: PropTypes.bool.isRequired, + icon: PropTypes.element.isRequired, + bgClass: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, +}; + CourseGradeFooter.propTypes = { passingGrade: PropTypes.number.isRequired, }; diff --git a/src/course-home/progress-tab/grades/course-grade/CurrentGradeTooltip.jsx b/src/course-home/progress-tab/grades/course-grade/CurrentGradeTooltip.jsx index 36ba44e9..8e1c6b29 100644 --- a/src/course-home/progress-tab/grades/course-grade/CurrentGradeTooltip.jsx +++ b/src/course-home/progress-tab/grades/course-grade/CurrentGradeTooltip.jsx @@ -13,6 +13,7 @@ const CurrentGradeTooltip = ({ tooltipClassName }) => { const courseId = useContextId(); const { + assignmentTypeGradeSummary, courseGrade: { isPassing, percent, @@ -25,6 +26,8 @@ const CurrentGradeTooltip = ({ tooltipClassName }) => { const isLocaleRtl = isRtl(getLocale()); + const hasHiddenGrades = assignmentTypeGradeSummary.some((assignmentType) => assignmentType.hasHiddenContribution !== 'none'); + if (isLocaleRtl) { currentGradeDirection = currentGrade < 50 ? '-' : ''; } @@ -56,6 +59,15 @@ const CurrentGradeTooltip = ({ tooltipClassName }) => { > {intl.formatMessage(messages.currentGradeLabel)} + + {hasHiddenGrades ? ` + ${intl.formatMessage(messages.hiddenScoreLabel)}` : ''} + ); }; diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummary.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummary.jsx index ffc5e2c8..6066997a 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummary.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummary.jsx @@ -10,14 +10,12 @@ const GradeSummary = () => { const courseId = useContextId(); const { - gradingPolicy: { - assignmentPolicies, - }, + assignmentTypeGradeSummary, } = useModel('progress', courseId); const [allOfSomeAssignmentTypeIsLocked, setAllOfSomeAssignmentTypeIsLocked] = useState(false); - if (assignmentPolicies.length === 0) { + if (assignmentTypeGradeSummary.length === 0) { return null; } 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 b6e5ceaf..44129521 100644 --- a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx +++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n'; import { DataTable } from '@openedx/paragon'; +import { Lock } from '@openedx/paragon/icons'; import { useContextId } from '../../../../data/hooks'; import { useModel } from '../../../../generic/model-store'; @@ -16,9 +17,7 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => { const courseId = useContextId(); const { - gradingPolicy: { - assignmentPolicies, - }, + assignmentTypeGradeSummary, gradesFeatureIsFullyLocked, sectionScores, } = useModel('progress', courseId); @@ -55,7 +54,7 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => { return false; }; - const gradeSummaryData = assignmentPolicies.map((assignment) => { + const gradeSummaryData = assignmentTypeGradeSummary.map((assignment) => { const { averageGrade, numDroppable, @@ -80,13 +79,24 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => { const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignmentType); const isLocaleRtl = isRtl(getLocale()); + let weightedGradeDisplay = `${getGradePercent(weightedGrade)}${isLocaleRtl ? '\u200f' : ''}%`; + let gradeDisplay = `${getGradePercent(averageGrade)}${isLocaleRtl ? '\u200f' : ''}%`; + + if (assignment.hasHiddenContribution === 'all') { + gradeDisplay = ; + weightedGradeDisplay = ; + } else if (assignment.hasHiddenContribution === 'some') { + gradeDisplay = `${getGradePercent(averageGrade)}${isLocaleRtl ? '\u200f' : ''}% + ${intl.formatMessage(messages.hiddenScoreLabel)}`; + weightedGradeDisplay = `${getGradePercent(weightedGrade)}${isLocaleRtl ? '\u200f' : ''}% + ${intl.formatMessage(messages.hiddenScoreLabel)}`; + } + return { type: { footnoteId, footnoteMarker, type: assignmentType, locked, }, weight: { weight: `${(weight * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked }, - grade: { grade: `${getGradePercent(averageGrade)}${isLocaleRtl ? '\u200f' : ''}%`, locked }, - weightedGrade: { weightedGrade: `${getGradePercent(weightedGrade)}${isLocaleRtl ? '\u200f' : ''}%`, locked }, + grade: { grade: gradeDisplay, locked }, + weightedGrade: { weightedGrade: weightedGradeDisplay, locked }, }; }); const getAssignmentTypeCell = (value) => ( @@ -102,6 +112,16 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => { return ( <> +
    +
  • + {intl.formatMessage(messages.hiddenScoreLabel)}: + {intl.formatMessage(messages.hiddenScoreInfoText)} +
  • +
  • + : + {` ${intl.formatMessage(messages.hiddenScoreLockInfoText)}`} +
  • +
{ 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 = useContextId(); const { @@ -36,8 +21,16 @@ const GradeSummaryTableFooter = () => { isPassing, percent, }, + finalGrades, } = useModel('progress', courseId); + const getGradePercent = (grade) => { + const percentage = grade * 100; + return Number.isInteger(percentage) ? percentage.toFixed(0) : percentage.toFixed(2); + }; + + const rawGrade = getGradePercent(finalGrades); + const bgColor = isPassing ? 'bg-success-100' : 'bg-warning-100'; const totalGrade = (percent * 100).toFixed(0); diff --git a/src/course-home/progress-tab/grades/messages.ts b/src/course-home/progress-tab/grades/messages.ts index a052096c..27543744 100644 --- a/src/course-home/progress-tab/grades/messages.ts +++ b/src/course-home/progress-tab/grades/messages.ts @@ -21,6 +21,11 @@ const messages = defineMessages({ defaultMessage: 'Your current grade is {currentGrade}%. A weighted grade of {passingGrade}% is required to pass in this course.', description: 'Alt text for the grade chart bar', }, + courseGradeFooterDueDateNotice: { + id: 'progress.courseGrade.footer.dueDateNotice', + defaultMessage: 'Some assignment scores are not yet included in your total grade. These grades will be released by {dueDate}.', + description: 'This is shown when there are pending assignments with a due date in the future', + }, courseGradeFooterGenericPassing: { id: 'progress.courseGrade.footer.generic.passing', defaultMessage: 'You’re currently passing this course', @@ -148,6 +153,21 @@ const messages = defineMessages({ + "Your weighted grade is what's used to determine if you pass the course.", description: 'The content of (tip box) for the grade summary section', }, + hiddenScoreLabel: { + id: 'progress.hiddenScoreLabel', + defaultMessage: 'Hidden Scores', + description: 'Text to indicate that some scores are hidden', + }, + hiddenScoreInfoText: { + id: 'progress.hiddenScoreInfoText', + defaultMessage: 'Scores from assignments that count toward your final grade but some are not shown here.', + description: 'Information text about hidden score label', + }, + hiddenScoreLockInfoText: { + id: 'progress.hiddenScoreLockInfoText', + defaultMessage: 'Scores for an assignment type are hidden but still counted toward the course grade.', + description: 'Information text about hidden score label when learners have limited access to grades feature', + }, noAccessToAssignmentType: { id: 'progress.noAcessToAssignmentType', defaultMessage: 'You do not have access to assignments of type {assignmentType}', diff --git a/src/course-home/progress-tab/utils.ts b/src/course-home/progress-tab/utils.ts index 29dd42de..aeb40f50 100644 --- a/src/course-home/progress-tab/utils.ts +++ b/src/course-home/progress-tab/utils.ts @@ -5,3 +5,15 @@ export const showUngradedAssignments = () => ( getConfig().SHOW_UNGRADED_ASSIGNMENT_PROGRESS === 'true' || getConfig().SHOW_UNGRADED_ASSIGNMENT_PROGRESS === true ); + +export const getLatestDueDateInFuture = (assignmentTypeGradeSummary) => { + let latest = null; + assignmentTypeGradeSummary.forEach((assignment) => { + const assignmentLastGradePublishDate = assignment.lastGradePublishDate; + if (assignmentLastGradePublishDate && (!latest || new Date(assignmentLastGradePublishDate) > new Date(latest)) + && new Date(assignmentLastGradePublishDate) > new Date()) { + latest = assignmentLastGradePublishDate; + } + }); + return latest; +};