diff --git a/lms/djangoapps/course_home_api/progress/api.py b/lms/djangoapps/course_home_api/progress/api.py index dab4ee91078b..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': policy.get('weight', 0.0), + 'weight': _to_float(policy.get('weight')), 'short_label': policy.get('short_label', ''), - 'num_droppable': policy.get('drop_count', 0), - 'num_total': policy.get('min_count', 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 4418dcb36fe4..bcc4dffef3b2 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,24 @@ 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_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), + _make_subsection('Homework', 0, 1, ShowCorrectness.ALWAYS), + ], + {'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}, + ), ] @@ -187,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'] == 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 )