Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Unreleased

* nothing unreleased

[8.0.15] - 2026-05-15
---------------------
* feat: add GradeEventContextEnricher pipeline step for grade analytics (ENT-11563)

[8.0.14] - 2026-05-14
---------------------
* feat: Add basic logging for all enterprise filter pipeline steps (ENT-11830)
Expand Down
2 changes: 1 addition & 1 deletion enterprise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Your project description goes here.
"""

__version__ = "8.0.14"
__version__ = "8.0.15"
3 changes: 3 additions & 0 deletions enterprise/filters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Filter pipeline step implementations for edx-enterprise openedx-filters integrations.
"""
47 changes: 47 additions & 0 deletions enterprise/filters/grades.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""
Pipeline step for enriching grade analytics event context.
"""
import copy
from typing import Any

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: dict, user_id: int, course_id: str) -> dict[str, Any]: # 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): the course key for the grade event.

Returns:
dict: updated pipeline data with the enriched ``context`` dict::
Comment thread
pwnage101 marked this conversation as resolved.

{
"context": <enriched context>,
"user_id": <unchanged>,
"course_id": <unchanged>,
}
"""
uuids = EnterpriseCourseEnrollment.get_enterprise_uuids_with_user_and_course(user_id, course_id)
Copy link
Copy Markdown
Contributor Author

@pwnage101 pwnage101 May 19, 2026

Choose a reason for hiding this comment

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

Please start this pipeline step with a log line to mirror all the other enterprise pipeline steps and improve stage/prod testing experience. Examples:

Be sure to not log PII. In this case, probably just the user_id and course_id are sufficient (exclude the context to keep log size down).

if uuids:
context = copy.deepcopy(context) # create a copy to avoid altering the passed-in value.
# Warning: Selecting the first element is not likely deterministic!
context["enterprise_uuid"] = str(uuids[0])
return {"context": context, "user_id": user_id, "course_id": course_id}
4 changes: 4 additions & 0 deletions enterprise/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
"fail_silently": False,
"pipeline": ["enterprise.filters.dashboard.DashboardContextEnricher"],
},
"org.openedx.learning.grade.context.requested.v1": {
"fail_silently": False,
"pipeline": ["enterprise.filters.grades.GradeEventContextEnricher"],
},
}


Expand Down
1 change: 1 addition & 0 deletions tests/filters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for enterprise filter pipeline steps."""
73 changes: 73 additions & 0 deletions tests/filters/test_grades.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
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(
filter_type="org.openedx.learning.grade.context.requested.v1",
running_pipeline=[],
)
Comment thread
kiram15 marked this conversation as resolved.

@patch("enterprise.models.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)},
"user_id": 7,
"course_id": "course-v1:org+course+run",
}
mock_get_uuids.assert_called_once_with(7, "course-v1:org+course+run")

@patch("enterprise.models.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,
"user_id": 99,
"course_id": "course-v1:org+course+run",
}
assert "enterprise_uuid" not in result["context"]

@patch("enterprise.models.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)
Loading