Skip to content

Commit 819f1af

Browse files
committed
feat: add new subsection visibility option to not show score but include grade in final calculation in progress page
1 parent 7fccf77 commit 819f1af

File tree

9 files changed

+418
-63
lines changed

9 files changed

+418
-63
lines changed

src/course-home/data/api.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,40 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
33
import { logInfo } from '@edx/frontend-platform/logging';
44
import { appendBrowserTimezoneToUrl } from '../../utils';
55

6-
const calculateAssignmentTypeGrades = (points, assignmentWeight, numDroppable) => {
6+
const calculateAssignmentTypeGrades = (points, visibilities, assignmentWeight, numDroppable) => {
77
let dropCount = numDroppable;
88
// Drop the lowest grades
99
while (dropCount && points.length >= dropCount) {
1010
const lowestScore = Math.min(...points);
1111
const lowestScoreIndex = points.indexOf(lowestScore);
1212
points.splice(lowestScoreIndex, 1);
13+
visibilities.splice(lowestScoreIndex, 1);
1314
dropCount--;
1415
}
1516
let averageGrade = 0;
1617
let weightedGrade = 0;
18+
let totalWeightedGrade = 0;
19+
1720
if (points.length) {
18-
// Calculate the average grade for the assignment and round it. This rounding is not ideal and does not accurately
19-
// reflect what a learner's grade would be, however, we must have parity with the current grading behavior that
20-
// exists in edx-platform.
21-
averageGrade = (points.reduce((a, b) => a + b, 0) / points.length).toFixed(4);
22-
weightedGrade = averageGrade * assignmentWeight;
21+
// Scores for visible grades (exclude never_but_include_grade)
22+
const visibleScores = points.filter(
23+
(_, idx) => visibilities[idx] !== 'never_but_include_grade',
24+
);
25+
26+
// Average all scores (for totalWeightedGrade)
27+
const overallAverage = parseFloat(
28+
(points.reduce((a, b) => a + b, 0) / points.length).toFixed(4),
29+
);
30+
totalWeightedGrade = overallAverage * assignmentWeight;
31+
if (visibleScores.length) {
32+
const visibleAverage = parseFloat(
33+
(visibleScores.reduce((a, b) => a + b, 0) / points.length).toFixed(4),
34+
);
35+
averageGrade = visibleAverage;
36+
weightedGrade = averageGrade * assignmentWeight;
37+
}
2338
}
24-
return { averageGrade, weightedGrade };
39+
return { averageGrade, weightedGrade, totalWeightedGrade };
2540
};
2641

2742
function normalizeAssignmentPolicies(assignmentPolicies, sectionScores) {
@@ -33,6 +48,7 @@ function normalizeAssignmentPolicies(assignmentPolicies, sectionScores) {
3348
grades: Array(assignment.numTotal).fill(0),
3449
numAssignmentsCreated: 0,
3550
numTotalExpectedAssignments: assignment.numTotal,
51+
visibility: Array(assignment.numTotal),
3652
};
3753
});
3854

@@ -45,6 +61,7 @@ function normalizeAssignmentPolicies(assignmentPolicies, sectionScores) {
4561
assignmentType,
4662
numPointsEarned,
4763
numPointsPossible,
64+
showCorrectness,
4865
} = subsection;
4966

5067
// If a subsection's assignment type does not match an assignment policy in Studio,
@@ -64,17 +81,20 @@ function normalizeAssignmentPolicies(assignmentPolicies, sectionScores) {
6481
// Remove a placeholder grade so long as the number of recorded created assignments is less than the number
6582
// of expected assignments
6683
gradeByAssignmentType[assignmentType].grades.shift();
84+
gradeByAssignmentType[assignmentType].visibility.shift();
6785
}
6886
// Add the graded assignment to the list
6987
gradeByAssignmentType[assignmentType].grades.push(numPointsEarned ? numPointsEarned / numPointsPossible : 0);
7088
// Record the created assignment
7189
gradeByAssignmentType[assignmentType].numAssignmentsCreated = numAssignmentsCreated;
90+
gradeByAssignmentType[assignmentType].visibility.push(showCorrectness);
7291
});
7392
});
7493

7594
return assignmentPolicies.map((assignment) => {
76-
const { averageGrade, weightedGrade } = calculateAssignmentTypeGrades(
95+
const { averageGrade, weightedGrade, totalWeightedGrade } = calculateAssignmentTypeGrades(
7796
gradeByAssignmentType[assignment.type].grades,
97+
gradeByAssignmentType[assignment.type].visibility,
7898
assignment.weight,
7999
assignment.numDroppable,
80100
);
@@ -86,6 +106,7 @@ function normalizeAssignmentPolicies(assignmentPolicies, sectionScores) {
86106
type: assignment.type,
87107
weight: assignment.weight,
88108
weightedGrade,
109+
totalWeightedGrade,
89110
};
90111
});
91112
}

src/course-home/progress-tab/ProgressTab.test.jsx

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,187 @@ describe('Progress Tab', () => {
697697
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
698698
expect(screen.getByRole('row', { name: 'Homework 1 100% 0% 0%' })).toBeInTheDocument();
699699
});
700+
701+
it('shows lock icon when all subsections of assignment type are never_but_include_grade', async () => {
702+
setTabData({
703+
grading_policy: {
704+
assignment_policies: [
705+
{
706+
num_droppable: 0,
707+
num_total: 2,
708+
short_label: 'HW',
709+
type: 'Homework',
710+
weight: 1,
711+
},
712+
],
713+
grade_range: {
714+
pass: 0.75,
715+
},
716+
},
717+
section_scores: [
718+
{
719+
display_name: 'Section 1',
720+
subsections: [
721+
{
722+
assignment_type: 'Homework',
723+
display_name: 'Subsection 1',
724+
learner_has_access: true,
725+
has_graded_assignment: true,
726+
num_points_earned: 1,
727+
num_points_possible: 2,
728+
percent_graded: 1.0,
729+
show_correctness: 'never_but_include_grade',
730+
show_grades: true,
731+
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/subsection1',
732+
},
733+
{
734+
assignment_type: 'Homework',
735+
display_name: 'Subsection 2',
736+
learner_has_access: true,
737+
has_graded_assignment: true,
738+
num_points_earned: 1,
739+
num_points_possible: 2,
740+
percent_graded: 1.0,
741+
show_correctness: 'never_but_include_grade',
742+
show_grades: true,
743+
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/subsection2',
744+
},
745+
],
746+
},
747+
],
748+
});
749+
await fetchAndRender();
750+
// Should show lock icon for grade and weighted grade
751+
expect(screen.getAllByTestId('lock-icon')).toHaveLength(2);
752+
});
753+
754+
it.only('shows percent plus hidden grades when some subsections of assignment type are never_but_include_grade', async () => {
755+
setTabData({
756+
grading_policy: {
757+
assignment_policies: [
758+
{
759+
num_droppable: 0,
760+
num_total: 2,
761+
short_label: 'HW',
762+
type: 'Homework',
763+
weight: 1,
764+
},
765+
],
766+
grade_range: {
767+
pass: 0.75,
768+
},
769+
},
770+
section_scores: [
771+
{
772+
display_name: 'Section 1',
773+
subsections: [
774+
{
775+
assignment_type: 'Homework',
776+
display_name: 'Subsection 1',
777+
learner_has_access: true,
778+
has_graded_assignment: true,
779+
num_points_earned: 1,
780+
num_points_possible: 2,
781+
percent_graded: 1.0,
782+
show_correctness: 'never_but_include_grade',
783+
show_grades: true,
784+
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/subsection1',
785+
},
786+
{
787+
assignment_type: 'Homework',
788+
display_name: 'Subsection 2',
789+
learner_has_access: true,
790+
has_graded_assignment: true,
791+
num_points_earned: 1,
792+
num_points_possible: 2,
793+
percent_graded: 1.0,
794+
show_correctness: 'always',
795+
show_grades: true,
796+
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/subsection2',
797+
},
798+
],
799+
},
800+
],
801+
});
802+
await fetchAndRender();
803+
// Should show percent + hidden scores for grade and weighted grade
804+
const hiddenScoresCells = screen.getAllByText(/% \+ Hidden Scores/);
805+
expect(hiddenScoresCells).toHaveLength(2);
806+
// Only correct visible scores should be shown (from subsection2)
807+
// The correct visible score is 1/4 = 0.25 -> 25%
808+
expect(hiddenScoresCells[0]).toHaveTextContent('25% + Hidden Scores');
809+
expect(hiddenScoresCells[1]).toHaveTextContent('25% + Hidden Scores');
810+
});
811+
812+
it('displays a warning message with the latest due date when not all assignment scores are included in the total grade', async () => {
813+
setTabData({
814+
grading_policy: {
815+
assignment_policies: [
816+
{
817+
num_droppable: 0,
818+
num_total: 2,
819+
short_label: 'HW',
820+
type: 'Homework',
821+
weight: 1,
822+
},
823+
],
824+
grade_range: {
825+
pass: 0.75,
826+
},
827+
},
828+
section_scores: [
829+
{
830+
display_name: 'Section 1',
831+
subsections: [
832+
{
833+
assignment_type: 'Homework',
834+
display_name: 'Subsection 1',
835+
due: tomorrow.toISOString(),
836+
learner_has_access: true,
837+
has_graded_assignment: true,
838+
num_points_earned: 1,
839+
num_points_possible: 2,
840+
percent_graded: 1.0,
841+
show_correctness: 'never_but_include_grade',
842+
show_grades: true,
843+
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/subsection1',
844+
},
845+
{
846+
assignment_type: 'Homework',
847+
display_name: 'Subsection 2',
848+
due: null,
849+
learner_has_access: true,
850+
has_graded_assignment: true,
851+
num_points_earned: 1,
852+
num_points_possible: 2,
853+
percent_graded: 1.0,
854+
show_correctness: 'always',
855+
show_grades: true,
856+
url: 'http://learning.edx.org/course/course-v1:edX+Test+run/subsection2',
857+
},
858+
],
859+
},
860+
],
861+
});
862+
863+
await fetchAndRender();
864+
865+
const formattedDateTime = new Intl.DateTimeFormat('en', {
866+
year: 'numeric',
867+
month: 'long',
868+
day: 'numeric',
869+
hour: 'numeric',
870+
minute: 'numeric',
871+
timeZoneName: 'short',
872+
}).format(tomorrow);
873+
874+
expect(
875+
screen.getByText(
876+
`Some assignment scores are not yet included in your total grade. These grades will be released by ${formattedDateTime}.`,
877+
),
878+
).toBeInTheDocument();
879+
});
880+
700881
it('calculates grades correctly when number of droppable assignments is less than total number of assignments', async () => {
701882
await fetchAndRender();
702883
expect(screen.getByText('Grade summary')).toBeInTheDocument();

0 commit comments

Comments
 (0)