From ed7f1b410d25727598ccb3685e85ae685ffa2a43 Mon Sep 17 00:00:00 2001 From: Troy Sankey Date: Mon, 2 Mar 2026 15:43:20 -0800 Subject: [PATCH] feat: add GradeEventContextRequested filter for grade analytics enrichment Introduces a new GradeEventContextRequested filter (org.openedx.learning.grade.context.requested.v1) that allows pipeline steps to enrich the context dict emitted with grade analytics events. Adds unit tests confirming correct filter type and pipeline behavior. ENT-11563 Co-Authored-By: Claude Sonnet 4.6 --- openedx_filters/learning/filters.py | 44 ++++++++++++++++++- .../learning/tests/test_filters.py | 33 ++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/openedx_filters/learning/filters.py b/openedx_filters/learning/filters.py index 6db44fd3..080f84ed 100644 --- a/openedx_filters/learning/filters.py +++ b/openedx_filters/learning/filters.py @@ -2,7 +2,7 @@ Package where filters related to the learning architectural subdomain are implemented. """ -from typing import Any, Optional +from typing import Any, Optional, Union from django.db.models.query import QuerySet from django.http import HttpResponse, QueryDict @@ -1445,3 +1445,45 @@ def run_filter(cls, schedules: QuerySet) -> QuerySet | None: """ data = super().run_pipeline(schedules=schedules) return data.get("schedules") + + +class GradeEventContextRequested(OpenEdxPublicFilter): + """ + Filter used to enrich the context dict emitted with a grade analytics event. + + Purpose: + This filter is triggered just before a grade-related analytics event is emitted, + allowing pipeline steps to inject additional key-value pairs into the event + tracking context. + + Filter Type: + org.openedx.learning.grade.context.requested.v1 + + Trigger: + - Repository: openedx/edx-platform + - Path: lms/djangoapps/grades/events.py + - Function or Method: course_grade_passed_first_time + """ + + filter_type = "org.openedx.learning.grade.context.requested.v1" + + @classmethod + def run_filter( + cls, + context: dict, + user_id: int, + course_id: Union[str, Any], + ) -> Optional[dict]: + """ + Process the context dict using the configured pipeline steps. + + Arguments: + context (dict): the event tracking context dict to be enriched. + user_id (int): the ID of the user whose grade event is being emitted. + course_id (str or CourseKey): the course identifier for the grade event. + + Returns: + dict: the context dict, possibly enriched by pipeline steps. + """ + data = super().run_pipeline(context=context, user_id=user_id, course_id=course_id) + return data.get("context") diff --git a/openedx_filters/learning/tests/test_filters.py b/openedx_filters/learning/tests/test_filters.py index b4ccf69a..d1f90572 100644 --- a/openedx_filters/learning/tests/test_filters.py +++ b/openedx_filters/learning/tests/test_filters.py @@ -22,6 +22,7 @@ CourseRunAPIRenderStarted, CourseUnenrollmentStarted, DashboardRenderStarted, + GradeEventContextRequested, IDVPageURLRequested, InstructorDashboardRenderStarted, ORASubmissionViewRenderStarted, @@ -801,3 +802,35 @@ def test_schedule_requested(self): result = ScheduleQuerySetRequested.run_filter(schedules) self.assertEqual(schedules, result) + + +class TestGradeEventContextRequestedFilter(TestCase): + """ + Tests for the GradeEventContextRequested filter. + """ + + def test_run_filter_returns_context_unchanged_when_no_pipeline(self): + """ + When no pipeline steps are configured, run_filter returns the original context. + """ + context = {"course_id": "course-v1:org+course+run"} + user_id = 42 + course_id = "course-v1:org+course+run" + + with patch.object(GradeEventContextRequested, "run_pipeline", return_value={"context": context}): + result = GradeEventContextRequested.run_filter( + context=context, + user_id=user_id, + course_id=course_id, + ) + + self.assertEqual(result, context) + + def test_filter_type(self): + """ + Confirm the filter type string is correct. + """ + self.assertEqual( + GradeEventContextRequested.filter_type, + "org.openedx.learning.grade.context.requested.v1", + )