Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------------

Expand Down
2 changes: 1 addition & 1 deletion openedx_filters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
52 changes: 49 additions & 3 deletions openedx_filters/learning/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated spelling change

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'.
Expand Down Expand Up @@ -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"]
34 changes: 31 additions & 3 deletions openedx_filters/learning/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
CourseEnrollmentStarted,
CourseEnrollmentViewStarted,
CourseHomeUrlCreationStarted,
CourseModePriceRequested,
CourseRunAPIRenderStarted,
CourseStartDateValidationFailed,
CourseUnenrollmentStarted,
Expand Down Expand Up @@ -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()
Expand All @@ -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