diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 09aae76..1d926b5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -33,6 +33,13 @@ Unreleased ---------- .. scriv-insert-here +[3.5.0] - 2026-05-22 + +Added +~~~~~ + +* Added new ``DiscountEligibilityCheckRequested`` filter + [3.4.0] - 2026-05-07 Added diff --git a/openedx_filters/__init__.py b/openedx_filters/__init__.py index bd86af1..3315a2a 100644 --- a/openedx_filters/__init__.py +++ b/openedx_filters/__init__.py @@ -6,7 +6,7 @@ from openedx_filters.filters import * -__version__ = "3.4.0" +__version__ = "3.5.0" if sys.version_info < (3, 12): # pragma: no cover warnings.warn( diff --git a/openedx_filters/learning/filters.py b/openedx_filters/learning/filters.py index 8ca1f2b..37c7b77 100644 --- a/openedx_filters/learning/filters.py +++ b/openedx_filters/learning/filters.py @@ -4,6 +4,7 @@ from typing import Any, Optional +from django.contrib.auth.base_user import AbstractBaseUser from django.db.models.query import QuerySet from django.http import HttpResponse, QueryDict from opaque_keys.edx.keys import CourseKey @@ -1598,3 +1599,51 @@ def run_filter(cls, context: dict, user_id: int, course_id: str) -> tuple[dict, """ data = super().run_pipeline(context=context, user_id=user_id, course_id=course_id) return data["context"], data["user_id"], data["course_id"] + + +class DiscountEligibilityCheckRequested(OpenEdxPublicFilter): + """ + Filter used to allow plugins to mark a user as ineligible for a course discount. + + Purpose: + This filter is triggered during discount applicability checks, just before the + final eligibility decision is returned to the caller. Pipeline steps may set + ``is_eligible`` to ``False`` to exclude a user from receiving a discount. + + Filter Type: + org.openedx.learning.discount.eligibility.check.requested.v1 + + Trigger: + - Repository: openedx/openedx-platform + - Path: openedx/features/discounts/applicability.py + - Function or Method: can_receive_discount, can_show_streak_discount_coupon + """ + + filter_type = "org.openedx.learning.discount.eligibility.check.requested.v1" + + @classmethod + def run_filter( + cls, + user: AbstractBaseUser, + course_key: CourseKey, + is_eligible: bool, + ) -> tuple[AbstractBaseUser, CourseKey, bool]: + """ + Process the inputs using the configured pipeline steps. + + Arguments: + user (User): the Django User being checked for discount eligibility. + course_key (CourseKey or course object): identifies the course. + is_eligible (bool): the current eligibility status before plugin evaluation. + + Returns: + tuple[User, CourseKey, bool]: + - User: the Django User object (unchanged). + - CourseKey: the course key (unchanged). + - bool: the (possibly overridden) eligibility flag. + """ + if is_eligible is False: + # If the user is already marked as ineligible, skip the pipeline to avoid unnecessary processing. + return user, course_key, is_eligible + data = super().run_pipeline(user=user, course_key=course_key, is_eligible=is_eligible) + return data["user"], data["course_key"], data["is_eligible"] diff --git a/openedx_filters/learning/tests/test_filters.py b/openedx_filters/learning/tests/test_filters.py index b537e87..17e84b2 100644 --- a/openedx_filters/learning/tests/test_filters.py +++ b/openedx_filters/learning/tests/test_filters.py @@ -23,6 +23,7 @@ CourseRunAPIRenderStarted, CourseUnenrollmentStarted, DashboardRenderStarted, + DiscountEligibilityCheckRequested, GradeEventContextRequested, IDVPageURLRequested, InstructorDashboardRenderStarted, @@ -962,3 +963,41 @@ def test_prevent_tabs_generation_exception(self, exception_class, attributes): exception = exception_class(**attributes) self.assertLessEqual(attributes.items(), exception.__dict__.items()) + + +class TestDiscountEligibilityCheckRequestedFilter(TestCase): + """ + Tests for the DiscountEligibilityCheckRequested filter. + """ + + def test_filter_type(self): + self.assertEqual( + DiscountEligibilityCheckRequested.filter_type, + "org.openedx.learning.discount.eligibility.check.requested.v1", + ) + + def test_run_filter_returns_false_when_pipeline_sets_ineligible(self): + user = Mock() + course_key = Mock() + + returned_user, returned_course_key, is_eligible = ( + DiscountEligibilityCheckRequested.run_filter(user, course_key, True) + ) + + self.assertTrue(is_eligible) + self.assertIs(returned_user, user) + self.assertIs(returned_course_key, course_key) + + def test_run_filter_skips_pipeline_when_already_ineligible(self): + user = Mock() + course_key = Mock() + + with patch("openedx_filters.tooling.OpenEdxPublicFilter.run_pipeline") as mock_run_pipeline: + returned_user, returned_course_key, is_eligible = ( + DiscountEligibilityCheckRequested.run_filter(user, course_key, False) + ) + + mock_run_pipeline.assert_not_called() + self.assertFalse(is_eligible) + self.assertIs(returned_user, user) + self.assertIs(returned_course_key, course_key)