diff --git a/src/course-home/data/__factories__/progressTabData.factory.js b/src/course-home/data/__factories__/progressTabData.factory.js index 1ff83241ce..3a3508f99e 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 88d684c83e..8254d4ef1e 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 a08bf8a40a..be99cab11d 100644 --- a/src/course-home/progress-tab/ProgressTab.test.jsx +++ b/src/course-home/progress-tab/ProgressTab.test.jsx @@ -661,29 +661,23 @@ 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', + num_droppable: 0, + num_total: 1, + short_label: 'Final', + type: 'Final Exam', weight: 1, }, ], @@ -691,19 +685,25 @@ describe('Progress Tab', () => { 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 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(); + // Should show lock icon for grade and weighted grade + expect(screen.getAllByTestId('lock-icon')).toHaveLength(2); }); - it('calculates grades correctly when number of droppable assignments is zero', async () => { + + it('shows percent plus hidden grades when some subsections of assignment type are hidden', async () => { setTabData({ grading_policy: { assignment_policies: [ @@ -719,41 +719,36 @@ describe('Progress Tab', () => { 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, + 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(); - 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(); + // 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('calculates grades correctly when number of total assignments is greater than the number of assignments created', async () => { + + 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: 5, // two assignments created in the factory, but 5 are expected per Studio settings + num_total: 2, short_label: 'HW', type: 'Homework', weight: 1, @@ -763,41 +758,36 @@ describe('Progress Tab', () => { 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, - }, - ], - 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(); - 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(); + + 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 e075411f25..6cada4cbb4 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 36ba44e926..8e1c6b2985 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 ffc5e2c890..6066997a9f 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 b6e5ceafbb..44129521e1 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 a052096c4f..2754374461 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 29dd42de85..aeb40f5099 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; +};