From 03a482a8d6bb112bf3c9744bacc139d36faeebab Mon Sep 17 00:00:00 2001 From: Nathan Sprenkle Date: Thu, 7 May 2026 16:57:11 -0400 Subject: [PATCH 1/2] fix: cast grading policy corretly as numbers (#273) Fixes an issue with some courses causing the progress page to crash --- lms/djangoapps/course_home_api/progress/api.py | 6 +++--- .../course_home_api/progress/tests/test_api.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/course_home_api/progress/api.py b/lms/djangoapps/course_home_api/progress/api.py index dab4ee91078b..843a8c91f7e1 100644 --- a/lms/djangoapps/course_home_api/progress/api.py +++ b/lms/djangoapps/course_home_api/progress/api.py @@ -130,10 +130,10 @@ def _build_policy_map(self) -> dict: policy_map = {} for policy in self.grading_policy.get('GRADER', []): policy_map[policy.get('type')] = { - 'weight': policy.get('weight', 0.0), + 'weight': float(policy.get('weight', 0.0) or 0.0), 'short_label': policy.get('short_label', ''), - 'num_droppable': policy.get('drop_count', 0), - 'num_total': policy.get('min_count', 0), + 'num_droppable': int(policy.get('drop_count', 0) or 0), + 'num_total': int(policy.get('min_count', 0) or 0), } return policy_map diff --git a/lms/djangoapps/course_home_api/progress/tests/test_api.py b/lms/djangoapps/course_home_api/progress/tests/test_api.py index 4418dcb36fe4..dbdfe71e3042 100644 --- a/lms/djangoapps/course_home_api/progress/tests/test_api.py +++ b/lms/djangoapps/course_home_api/progress/tests/test_api.py @@ -87,6 +87,15 @@ def _make_subsection(fmt, earned, possible, show_corr, *, due_delta_days=None, i ], {'avg': 0.0, 'weighted': 0.0, 'hidden': 'all', 'final': 0.0, 'last_grade_publish_date_days': 7}, ), + ( + 'string_typed_policy_counts', + {'type': 'Homework', 'weight': '1.0', 'drop_count': '1', 'min_count': '2', 'short_label': 'HW'}, + [ + _make_subsection('Homework', 1, 1, ShowCorrectness.ALWAYS), + _make_subsection('Homework', 0, 1, ShowCorrectness.ALWAYS), + ], + {'avg': 1.0, 'weighted': 1.0, 'hidden': 'none', 'final': 1.0}, + ), ] @@ -187,7 +196,7 @@ def test_aggregate_assignment_type_grade_summary_scenarios(self): assert row['average_grade'] == expected['avg'] assert row['weighted_grade'] == expected['weighted'] assert row['has_hidden_contribution'] == expected['hidden'] - assert row['num_droppable'] == policy['drop_count'] + assert row['num_droppable'] == int(policy['drop_count']) assert (row['last_grade_publish_date'] is not None) == ( 'last_grade_publish_date_days' in expected ) From 19fe61aafd9cc81ca7c76aa166415f9ffd64aba0 Mon Sep 17 00:00:00 2001 From: Nathan Sprenkle Date: Fri, 8 May 2026 13:05:41 -0400 Subject: [PATCH 2/2] fix: more robust casting of int / float in grading policy (#275) --- .../course_home_api/progress/api.py | 34 +++++++++++++++++-- .../progress/tests/test_api.py | 13 +++++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/course_home_api/progress/api.py b/lms/djangoapps/course_home_api/progress/api.py index 843a8c91f7e1..1d5f94cbf65c 100644 --- a/lms/djangoapps/course_home_api/progress/api.py +++ b/lms/djangoapps/course_home_api/progress/api.py @@ -17,6 +17,34 @@ User = get_user_model() +def _to_float(value, default=0.0) -> float: + """Parse a grading policy value as a float, returning default for any unparseable input.""" + try: + return float(value) + except (TypeError, ValueError): + return float(default) + + +def _to_int(value, default=0) -> int: + """ + Parse a grading policy value as an int, returning default for non-integer or unparseable input. + + Accepts string representations of both integers ('2') and whole-number floats ('2.0'). + Returns default for non-finite values (nan/inf), fractional floats ('2.9'), and + anything that cannot be parsed as a number. + """ + try: + parsed = float(value) + except (TypeError, ValueError): + return default + if not parsed.is_integer(): + return default + try: + return int(parsed) + except (OverflowError, ValueError): + return default + + @dataclass class _AssignmentBucket: """Holds scores and visibility info for one assignment type. @@ -130,10 +158,10 @@ def _build_policy_map(self) -> dict: policy_map = {} for policy in self.grading_policy.get('GRADER', []): policy_map[policy.get('type')] = { - 'weight': float(policy.get('weight', 0.0) or 0.0), + 'weight': _to_float(policy.get('weight')), 'short_label': policy.get('short_label', ''), - 'num_droppable': int(policy.get('drop_count', 0) or 0), - 'num_total': int(policy.get('min_count', 0) or 0), + 'num_droppable': _to_int(policy.get('drop_count')), + 'num_total': _to_int(policy.get('min_count')), } return policy_map diff --git a/lms/djangoapps/course_home_api/progress/tests/test_api.py b/lms/djangoapps/course_home_api/progress/tests/test_api.py index dbdfe71e3042..bcc4dffef3b2 100644 --- a/lms/djangoapps/course_home_api/progress/tests/test_api.py +++ b/lms/djangoapps/course_home_api/progress/tests/test_api.py @@ -88,7 +88,7 @@ def _make_subsection(fmt, earned, possible, show_corr, *, due_delta_days=None, i {'avg': 0.0, 'weighted': 0.0, 'hidden': 'all', 'final': 0.0, 'last_grade_publish_date_days': 7}, ), ( - 'string_typed_policy_counts', + 'string_int_typed_policy_counts', {'type': 'Homework', 'weight': '1.0', 'drop_count': '1', 'min_count': '2', 'short_label': 'HW'}, [ _make_subsection('Homework', 1, 1, ShowCorrectness.ALWAYS), @@ -96,6 +96,15 @@ def _make_subsection(fmt, earned, possible, show_corr, *, due_delta_days=None, i ], {'avg': 1.0, 'weighted': 1.0, 'hidden': 'none', 'final': 1.0}, ), + ( + 'string_float_typed_policy_counts', + {'type': 'Homework', 'weight': '1.0', 'drop_count': '1.0', 'min_count': '2.0', 'short_label': 'HW'}, + [ + _make_subsection('Homework', 1, 1, ShowCorrectness.ALWAYS), + _make_subsection('Homework', 0, 1, ShowCorrectness.ALWAYS), + ], + {'avg': 1.0, 'weighted': 1.0, 'hidden': 'none', 'final': 1.0}, + ), ] @@ -196,7 +205,7 @@ def test_aggregate_assignment_type_grade_summary_scenarios(self): assert row['average_grade'] == expected['avg'] assert row['weighted_grade'] == expected['weighted'] assert row['has_hidden_contribution'] == expected['hidden'] - assert row['num_droppable'] == int(policy['drop_count']) + assert row['num_droppable'] == int(float(policy['drop_count'])) assert (row['last_grade_publish_date'] is not None) == ( 'last_grade_publish_date_days' in expected )