diff --git a/lms/djangoapps/grades/events.py b/lms/djangoapps/grades/events.py index 1321b2e51d45..4e97013cf95d 100644 --- a/lms/djangoapps/grades/events.py +++ b/lms/djangoapps/grades/events.py @@ -15,6 +15,7 @@ UserPersonalData ) from openedx_events.learning.signals import CCX_COURSE_PASSING_STATUS_UPDATED, COURSE_PASSING_STATUS_UPDATED +from openedx_filters.learning.filters import GradeEventContextRequested from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.models import CourseEnrollment @@ -27,7 +28,6 @@ ) from lms.djangoapps.grades.signals.signals import SCHEDULE_FOLLOW_UP_SEGMENT_EVENT_FOR_COURSE_PASSED_FIRST_TIME from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from openedx.features.enterprise_support.context import get_enterprise_event_context log = getLogger(__name__) @@ -166,8 +166,11 @@ def course_grade_passed_first_time(user_id, course_id): """ event_name = COURSE_GRADE_PASSED_FIRST_TIME_EVENT_TYPE context = contexts.course_context_from_course_id(course_id) - context_enterprise = get_enterprise_event_context(user_id, course_id) - context.update(context_enterprise) + enriched_context = GradeEventContextRequested.run_filter( + context=context, user_id=user_id, course_id=course_id + ) + if enriched_context is not None: + context = enriched_context # TODO (AN-6134): remove this context manager with tracker.get_tracker().context(event_name, context): tracker.emit( diff --git a/lms/djangoapps/grades/tests/test_events.py b/lms/djangoapps/grades/tests/test_events.py index 9ef9abfb59a3..4f4f08a53973 100644 --- a/lms/djangoapps/grades/tests/test_events.py +++ b/lms/djangoapps/grades/tests/test_events.py @@ -3,6 +3,7 @@ """ from unittest import mock +from unittest.mock import patch from ccx_keys.locator import CCXLocator from django.utils.timezone import now @@ -23,13 +24,13 @@ from openedx_events.tests.utils import OpenEdxEventsTestMixin from common.djangoapps.student.tests.factories import AdminFactory, UserFactory +from common.test.utils import assert_dict_contains_subset from lms.djangoapps.ccx.models import CustomCourseForEdX from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory from lms.djangoapps.grades.models import PersistentCourseGrade from lms.djangoapps.grades.tests.utils import mock_passing_grade from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory -from common.test.utils import assert_dict_contains_subset class PersistentGradeEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): @@ -256,3 +257,48 @@ def test_ccx_course_passing_status_updated_emitted(self): }, event_receiver.call_args.kwargs, ) + + +class GradeEventContextFilterTest(SharedModuleStoreTestCase): + """ + Tests that course_grade_passed_first_time invokes the GradeEventContextRequested + filter instead of the old enterprise_support import. + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + + def setUp(self): + super().setUp() + self.user = UserFactory.create() + self.course = CourseFactory.create() + + @patch('lms.djangoapps.grades.events.GradeEventContextRequested.run_filter') + def test_filter_called_with_context(self, mock_run_filter): + """ + course_grade_passed_first_time should call GradeEventContextRequested.run_filter + and merge the returned context. + """ + enriched = {"org": "test_org", "enterprise_uuid": "abc-123"} + mock_run_filter.return_value = enriched + + from lms.djangoapps.grades.events import course_grade_passed_first_time + with patch('lms.djangoapps.grades.events.tracker'): + course_grade_passed_first_time(self.user.id, self.course.id) + + mock_run_filter.assert_called_once() + call_kwargs = mock_run_filter.call_args.kwargs + assert call_kwargs['user_id'] == self.user.id + assert str(call_kwargs['course_id']) == str(self.course.id) + + @patch('lms.djangoapps.grades.events.GradeEventContextRequested.run_filter') + def test_filter_none_return_leaves_context_intact(self, mock_run_filter): + """ + If run_filter returns None (fail_silently path), context is not overwritten. + """ + mock_run_filter.return_value = None + from lms.djangoapps.grades.events import course_grade_passed_first_time + with patch('lms.djangoapps.grades.events.tracker'): + # Should not raise even when filter returns None + course_grade_passed_first_time(self.user.id, self.course.id) diff --git a/lms/envs/common.py b/lms/envs/common.py index 917dd025e96f..4c6493489b4b 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3172,3 +3172,15 @@ def _should_send_certificate_events(settings): SSL_AUTH_DN_FORMAT_STRING = ( "/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}" ) + +# .. setting_name: OPEN_EDX_FILTERS_CONFIG +# .. setting_default: {} +# .. setting_description: Configuration dict for openedx-filters pipeline steps. +# Keys are filter type strings; values are dicts with 'fail_silently' (bool) and +# 'pipeline' (list of dotted-path strings to PipelineStep subclasses). +OPEN_EDX_FILTERS_CONFIG = { + "org.openedx.learning.grade.context.requested.v1": { + "fail_silently": True, + "pipeline": ["enterprise.filters.grades.GradeEventContextEnricher"], + }, +} diff --git a/lms/envs/production.py b/lms/envs/production.py index 63246821d954..a61704261153 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -84,6 +84,7 @@ def get_env_setting(setting): 'EVENT_BUS_PRODUCER_CONFIG', 'DEFAULT_FILE_STORAGE', 'STATICFILES_STORAGE', + 'OPEN_EDX_FILTERS_CONFIG', ] }) @@ -515,6 +516,19 @@ def get_env_setting(setting): _YAML_TOKENS.get('EVENT_BUS_PRODUCER_CONFIG', {}) ) +# Merge OPEN_EDX_FILTERS_CONFIG from YAML into the default defined in common.py. +# Pipeline steps from YAML are appended after steps defined in common.py. +# The fail_silently value from YAML takes precedence over the one in common.py. +for _filter_type, _filter_config in _YAML_TOKENS.get('OPEN_EDX_FILTERS_CONFIG', {}).items(): + if _filter_type in OPEN_EDX_FILTERS_CONFIG: + OPEN_EDX_FILTERS_CONFIG[_filter_type]['pipeline'].extend( + _filter_config.get('pipeline', []) + ) + if 'fail_silently' in _filter_config: + OPEN_EDX_FILTERS_CONFIG[_filter_type]['fail_silently'] = _filter_config['fail_silently'] + else: + OPEN_EDX_FILTERS_CONFIG[_filter_type] = _filter_config + ####################################################################################################################### # HEY! Don't add anything to the end of this file. # Add your defaults to common.py instead!