diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9d224c1..2acb624 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -35,6 +35,14 @@ Unreleased .. _changelog-3.7.0: +[3.8.0] - 2026-07-07 +--------------------- + +Added +~~~~~ + +* Added new ``CourseModePriceRequested`` filter + [3.7.0] - 2026-06-22 --------------------- diff --git a/openedx_filters/__init__.py b/openedx_filters/__init__.py index be96be0..0cd28cc 100644 --- a/openedx_filters/__init__.py +++ b/openedx_filters/__init__.py @@ -6,7 +6,7 @@ from openedx_filters.filters import * -__version__ = "3.7.0" +__version__ = "3.8.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 4809a6d..5873ea1 100644 --- a/openedx_filters/learning/filters.py +++ b/openedx_filters/learning/filters.py @@ -1095,14 +1095,14 @@ def run_filter( Process the inputs using the configured pipeline steps to modify the rendering of a vertical block. Arguments: - block (VerticalBlock): The VeriticalBlock instance which is being rendered. + block (VerticalBlock): The VerticalBlock instance which is being rendered. fragment (web_fragments.Fragment): The web-fragment containing the rendered content of VerticalBlock. context (dict): rendering context values like is_mobile_app, show_title..etc. view (str): the rendering view. Can be either 'student_view', or 'public_view'. Returns: - tuple[VeticalBlock, web_fragments.Fragment, dict, str]: - - VerticalBlock: The VeriticalBlock instance which is being rendered. + tuple[VerticalBlock, web_fragments.Fragment, dict, str]: + - VerticalBlock: The VerticalBlock instance which is being rendered. - web_fragments.Fragment: The web-fragment containing the rendered content of VerticalBlock. - dict: rendering context values like is_mobile_app, show_title..etc. - str: the rendering view. Can be either 'student_view', or 'public_view'. @@ -1890,3 +1890,49 @@ def run_filter( """ data = super().run_pipeline(user=user, course_key=course_key) return data["user"], data["course_key"] + + +class CourseModePriceRequested(OpenEdxPublicFilter): + """ + Filter used to determine the price a learner should be charged for a course mode. + + Purpose: + This filter is triggered when the price for a course mode checkout needs to be + calculated. Pipeline steps can adjust the price and apply discounts, like + enterprise-negotiated pricing. + + Filter Type: + org.openedx.learning.course_mode.price.requested.v1 + + Trigger: + - Repository: openedx/openedx-platform + - Path: common/djangoapps/course_modes/views.py + - Function or Method: ChooseModeView.get + """ + + filter_type = "org.openedx.learning.course_mode.price.requested.v1" + + @classmethod + def run_filter( + cls, + user: Any, + course_mode_data: Any, + price: int + ) -> tuple[Any, Any, int]: + """ + Process and possibly adjust the price through the configured pipeline steps. + + Arguments: + user (Any): The Django User object. + course_mode_data (Any): The selected course mode object. + price (int): Numerical price. + + Returns: + tuple[Any, Any, int]: + - user (Any): django User object (unchanged) + - course_mode_data (Any): The selected course mode object (unchanged) + - price (int): Price (possibly adjusted) + + """ + data = super().run_pipeline(user=user, course_mode_data=course_mode_data, price=price) + return data["user"], data["course_mode_data"], data["price"] diff --git a/openedx_filters/learning/tests/test_filters.py b/openedx_filters/learning/tests/test_filters.py index d0a57e1..4a7199f 100644 --- a/openedx_filters/learning/tests/test_filters.py +++ b/openedx_filters/learning/tests/test_filters.py @@ -23,6 +23,7 @@ CourseEnrollmentStarted, CourseEnrollmentViewStarted, CourseHomeUrlCreationStarted, + CourseModePriceRequested, CourseRunAPIRenderStarted, CourseStartDateValidationFailed, CourseUnenrollmentStarted, @@ -1121,8 +1122,8 @@ def test_run_filter_passes_through_user_and_course_key(self): DiscountEligibilityCheckRequested.run_filter(user, course_key) ) - self.assertIs(returned_user, user) - self.assertIs(returned_course_key, course_key) + assert returned_user == user + assert returned_course_key == course_key def test_run_filter_raises_discount_ineligible_from_pipeline(self): user = Mock() @@ -1139,4 +1140,31 @@ def test_run_filter_raises_discount_ineligible_from_pipeline(self): def test_discount_ineligible_exception_stores_message(self): exc = DiscountEligibilityCheckRequested.DiscountIneligible("Enterprise contract prohibits discount.") - self.assertEqual(exc.message, "Enterprise contract prohibits discount.") + assert exc.message == "Enterprise contract prohibits discount." + + +class TestCourseModeFilters(TestCase): + """ + Test class to verify standard behavior of the CourseModePriceRequested filter. + """ + + def test_course_mode_price_requested(self): + """ + Test CourseModePriceRequested filter behavior under normal conditions. + + Expected behavior: + - The filter must have the signature specified. + - The filter should return the (possibly discounted) price. + """ + user = Mock() + course_mode_data = Mock() + price = 100 + assert CourseModePriceRequested.filter_type == "org.openedx.learning.course_mode.price.requested.v1" + + result_user, result_course_mode_data, result_price = CourseModePriceRequested.run_filter( + user, course_mode_data, price + ) + + assert user == result_user + assert course_mode_data == result_course_mode_data + assert price == result_price