diff --git a/enterprise/filters/__init__.py b/enterprise/filters/__init__.py new file mode 100644 index 000000000..87ba36052 --- /dev/null +++ b/enterprise/filters/__init__.py @@ -0,0 +1,3 @@ +""" +Filter pipeline step implementations for edx-enterprise openedx-filters integrations. +""" diff --git a/enterprise/filters/grades.py b/enterprise/filters/grades.py new file mode 100644 index 000000000..fecf47e36 --- /dev/null +++ b/enterprise/filters/grades.py @@ -0,0 +1,39 @@ +""" +Pipeline step for enriching grade analytics event context. +""" +from openedx_filters.filters import PipelineStep + +from enterprise.models import EnterpriseCourseEnrollment + + +class GradeEventContextEnricher(PipelineStep): + """ + Enriches a grade analytics event context dict with the learner's enterprise UUID. + + This step is intended to be registered as a pipeline step for the + ``org.openedx.learning.grade.context.requested.v1`` filter. + + If the user is enrolled in the given course through an enterprise, the enterprise + UUID is added to the context under the key ``"enterprise_uuid"``. If the user has + no enterprise course enrollment, the context is returned unchanged. + """ + + def run_filter(self, context, user_id, course_id): # pylint: disable=arguments-differ + """ + Add enterprise UUID to the event context if the user has an enterprise enrollment. + + Arguments: + context (dict): the event tracking context dict. + user_id (int): the ID of the user whose grade event is being emitted. + course_id (str or CourseKey): the course key for the grade event. + + Returns: + dict: updated pipeline data with the enriched ``context`` dict. + """ + uuids = EnterpriseCourseEnrollment.get_enterprise_uuids_with_user_and_course( + str(user_id), + str(course_id), + ) + if uuids: + return {"context": {**context, "enterprise_uuid": str(uuids[0])}} + return {"context": context} diff --git a/requirements/base.in b/requirements/base.in index 1062e118a..f9f99f60c 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -32,6 +32,7 @@ jsondiff jsonfield openedx-atlas openedx-events +openedx-filters paramiko path.py pillow diff --git a/requirements/test.txt b/requirements/test.txt index 515d75150..10282b0c9 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -392,6 +392,8 @@ openedx-atlas==0.7.0 # via -r requirements/test-master.txt openedx-events==10.5.0 # via -r requirements/test-master.txt +openedx-filters==2.1.0 + # via -r requirements/base.in packaging==25.0 # via # -r requirements/test-master.txt diff --git a/tests/filters/__init__.py b/tests/filters/__init__.py new file mode 100644 index 000000000..5a5f3f9ac --- /dev/null +++ b/tests/filters/__init__.py @@ -0,0 +1 @@ +"""Tests for enterprise filter pipeline steps.""" diff --git a/tests/filters/test_grades.py b/tests/filters/test_grades.py new file mode 100644 index 000000000..e8c403ebd --- /dev/null +++ b/tests/filters/test_grades.py @@ -0,0 +1,74 @@ +""" +Tests for enterprise.filters.grades pipeline step. +""" +import uuid +from unittest.mock import patch + +from django.test import TestCase + +from enterprise.filters.grades import GradeEventContextEnricher + + +class TestGradeEventContextEnricher(TestCase): + """ + Tests for GradeEventContextEnricher pipeline step. + """ + + def _make_step(self): + return GradeEventContextEnricher( + "org.openedx.learning.grade.context.requested.v1", + [], + ) + + @patch( + "enterprise.filters.grades.EnterpriseCourseEnrollment" + ".get_enterprise_uuids_with_user_and_course" + ) + def test_enriches_context_when_enterprise_enrollment_found(self, mock_get_uuids): + """ + When an enterprise course enrollment exists, enterprise_uuid is added to context. + """ + enterprise_uuid = uuid.uuid4() + mock_get_uuids.return_value = [enterprise_uuid] + + step = self._make_step() + context = {"org": "TestOrg", "course_id": "course-v1:org+course+run"} + result = step.run_filter(context=context, user_id=7, course_id="course-v1:org+course+run") + + assert result == {"context": {**context, "enterprise_uuid": str(enterprise_uuid)}} + mock_get_uuids.assert_called_once_with("7", "course-v1:org+course+run") + + @patch( + "enterprise.filters.grades.EnterpriseCourseEnrollment" + ".get_enterprise_uuids_with_user_and_course" + ) + def test_returns_unchanged_context_when_no_enterprise_enrollment(self, mock_get_uuids): + """ + When no enterprise course enrollment exists, context is returned unchanged. + """ + mock_get_uuids.return_value = [] + + step = self._make_step() + context = {"org": "TestOrg"} + result = step.run_filter(context=context, user_id=99, course_id="course-v1:org+course+run") + + assert result == {"context": context} + assert "enterprise_uuid" not in result["context"] + + @patch( + "enterprise.filters.grades.EnterpriseCourseEnrollment" + ".get_enterprise_uuids_with_user_and_course" + ) + def test_uses_first_uuid_when_multiple_enrollments(self, mock_get_uuids): + """ + When multiple enterprise enrollments exist, only the first UUID is used. + """ + first_uuid = uuid.uuid4() + second_uuid = uuid.uuid4() + mock_get_uuids.return_value = [first_uuid, second_uuid] + + step = self._make_step() + context = {} + result = step.run_filter(context=context, user_id=1, course_id="course-v1:x+y+z") + + assert result["context"]["enterprise_uuid"] == str(first_uuid)