From b0fbc327409e334e192ff325a71b53efd4a87fea Mon Sep 17 00:00:00 2001 From: CP Date: Tue, 28 Apr 2026 10:27:56 -0400 Subject: [PATCH 1/4] Validate MIN_NUMBER_OF operator values Add validation to ensure MIN_NUMBER_OF operator values are numeric and >= 1. In courses/forms.py: add a minimum: 1 to the schema default, enforce presence and numeric/positive checks in ProgramAdminForm._validate_elective_value_presence, and fix a traversal bug to check the child's operator field. In courses/models.py: add ProgramRequirement.clean() to validate operator_value at the model level and raise descriptive ValidationError messages for non-numeric or <1 values. These changes prevent invalid/negative elective minimums at both form and model layers. --- courses/forms.py | 17 +++++++++++++++-- courses/models.py | 22 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/courses/forms.py b/courses/forms.py index 74583d9eeb..39b9aa562b 100644 --- a/courses/forms.py +++ b/courses/forms.py @@ -141,6 +141,7 @@ def program_requirements_schema(): "format": "number", "title": "Value", "default": 1, + "minimum": 1, "options": { "dependencies": { "node_type": ProgramRequirementNodeType.OPERATOR.value, @@ -269,7 +270,7 @@ def clean(self): # noqa: C901 def _validate_elective_value_presence(operator): """ Verifies that a Program's elective operator contains - a defined Value field. + a defined Value field that is not negative. Args: operator (dict): @@ -286,6 +287,7 @@ def _validate_elective_value_presence(operator): } ValidationError: operator_value does not exist. ValidationError: operator_value does exist but is empty. + ValidationError: operator_value is negative. """ if ( operator["data"]["operator"] @@ -300,6 +302,17 @@ def _validate_elective_value_presence(operator): raise ValidationError( '"Minimum # of" operator must have Value equal to 1 or more.' # noqa: EM101 ) + # Ensure the value is not negative + try: + value = int(operator["data"]["operator_value"]) + if value < 1: + raise ValidationError( + '"Minimum # of" operator must have Value equal to 1 or more.' # noqa: EM101 + ) + except (ValueError, TypeError): + raise ValidationError( + '"Minimum # of" operator must have a valid numeric Value equal to 1 or more.' # noqa: EM101 + ) def _validate_operator_title(operator): """Ensure Title is defined for every operator. @@ -363,7 +376,7 @@ def _validate_elective_value_and_child_courses( if child["data"]["node_type"] == "operator": _validate_operator_title(child) if ( - operator["data"]["operator"] + child["data"]["operator"] == ProgramRequirement.Operator.MIN_NUMBER_OF.value ): _validate_elective_value_presence(child) diff --git a/courses/models.py b/courses/models.py index ea099e99b5..9af8c44152 100644 --- a/courses/models.py +++ b/courses/models.py @@ -2279,6 +2279,28 @@ def is_operator(self): """True if the node is an operator""" return self.node_type == ProgramRequirementNodeType.OPERATOR + def clean(self): + """Validate the program requirement fields""" + super().clean() + + # Validate operator_value for MIN_NUMBER_OF operators + if ( + self.operator == self.Operator.MIN_NUMBER_OF + and self.operator_value is not None + ): + try: + value = int(self.operator_value) + if value < 1: + from django.core.exceptions import ValidationError + raise ValidationError({ + 'operator_value': 'Minimum # of value must be 1 or greater.' + }) + except (ValueError, TypeError): + from django.core.exceptions import ValidationError + raise ValidationError({ + 'operator_value': 'Minimum # of value must be a valid number.' + }) + @property def is_course(self): """True if the node references a course""" From ae8b72e63717c9406312e816be25512137e411ad Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:25:38 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- courses/models.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/courses/models.py b/courses/models.py index 9af8c44152..fa2a92ea47 100644 --- a/courses/models.py +++ b/courses/models.py @@ -2282,7 +2282,7 @@ def is_operator(self): def clean(self): """Validate the program requirement fields""" super().clean() - + # Validate operator_value for MIN_NUMBER_OF operators if ( self.operator == self.Operator.MIN_NUMBER_OF @@ -2292,14 +2292,16 @@ def clean(self): value = int(self.operator_value) if value < 1: from django.core.exceptions import ValidationError - raise ValidationError({ - 'operator_value': 'Minimum # of value must be 1 or greater.' - }) + + raise ValidationError( + {"operator_value": "Minimum # of value must be 1 or greater."} + ) except (ValueError, TypeError): from django.core.exceptions import ValidationError - raise ValidationError({ - 'operator_value': 'Minimum # of value must be a valid number.' - }) + + raise ValidationError( + {"operator_value": "Minimum # of value must be a valid number."} + ) @property def is_course(self): From 7b62b3ed5f1f115bf323e8ac62f47be8b02d9ba3 Mon Sep 17 00:00:00 2001 From: CP Date: Tue, 28 Apr 2026 13:36:02 -0400 Subject: [PATCH 3/4] ruff --- courses/forms.py | 2 +- courses/models.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/courses/forms.py b/courses/forms.py index 39b9aa562b..5c82538f2d 100644 --- a/courses/forms.py +++ b/courses/forms.py @@ -312,7 +312,7 @@ def _validate_elective_value_presence(operator): except (ValueError, TypeError): raise ValidationError( '"Minimum # of" operator must have a valid numeric Value equal to 1 or more.' # noqa: EM101 - ) + ) from None def _validate_operator_title(operator): """Ensure Title is defined for every operator. diff --git a/courses/models.py b/courses/models.py index 9af8c44152..05635bc006 100644 --- a/courses/models.py +++ b/courses/models.py @@ -2291,15 +2291,13 @@ def clean(self): try: value = int(self.operator_value) if value < 1: - from django.core.exceptions import ValidationError raise ValidationError({ 'operator_value': 'Minimum # of value must be 1 or greater.' }) except (ValueError, TypeError): - from django.core.exceptions import ValidationError raise ValidationError({ 'operator_value': 'Minimum # of value must be a valid number.' - }) + }) from None @property def is_course(self): From bd036c85d39faac21f9d637a8042b41a1f4b78e0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:37:05 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- courses/models.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/courses/models.py b/courses/models.py index cc95608582..362518d119 100644 --- a/courses/models.py +++ b/courses/models.py @@ -2291,13 +2291,13 @@ def clean(self): try: value = int(self.operator_value) if value < 1: - raise ValidationError({ - 'operator_value': 'Minimum # of value must be 1 or greater.' - }) + raise ValidationError( + {"operator_value": "Minimum # of value must be 1 or greater."} + ) except (ValueError, TypeError): - raise ValidationError({ - 'operator_value': 'Minimum # of value must be a valid number.' - }) from None + raise ValidationError( + {"operator_value": "Minimum # of value must be a valid number."} + ) from None @property def is_course(self):