Skip to content

Commit 68d471e

Browse files
pwnage101kiram15
andauthored
feat: add GradeEventContextEnricher pipeline step for grade analytics (#2543)
Co-authored-by: Kira Miller <31229189+kiram15@users.noreply.github.com>
1 parent 2216a75 commit 68d471e

7 files changed

Lines changed: 141 additions & 1 deletion

File tree

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ Unreleased
1818

1919
* nothing unreleased
2020

21+
[8.0.15] - 2026-05-15
22+
---------------------
23+
* feat: add GradeEventContextEnricher pipeline step for grade analytics (ENT-11563)
24+
2125
[8.0.14] - 2026-05-14
2226
---------------------
2327
* feat: Add basic logging for all enterprise filter pipeline steps (ENT-11830)

enterprise/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Your project description goes here.
33
"""
44

5-
__version__ = "8.0.14"
5+
__version__ = "8.0.15"

enterprise/filters/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Filter pipeline step implementations for edx-enterprise openedx-filters integrations.
3+
"""

enterprise/filters/grades.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""
2+
Pipeline step for enriching grade analytics event context.
3+
"""
4+
import copy
5+
import logging
6+
from typing import Any
7+
8+
from openedx_filters.filters import PipelineStep
9+
10+
from enterprise.models import EnterpriseCourseEnrollment
11+
12+
log = logging.getLogger(__name__)
13+
14+
15+
class GradeEventContextEnricher(PipelineStep):
16+
"""
17+
Enriches a grade analytics event context dict with the learner's enterprise UUID.
18+
19+
This step is intended to be registered as a pipeline step for the
20+
``org.openedx.learning.grade.context.requested.v1`` filter.
21+
22+
If the user is enrolled in the given course through an enterprise, the enterprise
23+
UUID is added to the context under the key ``"enterprise_uuid"``. If the user has
24+
no enterprise course enrollment, the context is returned unchanged.
25+
"""
26+
27+
def run_filter(self, context: dict, user_id: int, course_id: str) -> dict[str, Any]: # pylint: disable=arguments-differ
28+
"""
29+
Add enterprise UUID to the event context if the user has an enterprise enrollment.
30+
31+
Arguments:
32+
context (dict): the event tracking context dict.
33+
user_id (int): the ID of the user whose grade event is being emitted.
34+
course_id (str): the course key for the grade event.
35+
36+
Returns:
37+
dict: updated pipeline data with the enriched ``context`` dict::
38+
39+
{
40+
"context": <enriched context>,
41+
"user_id": <unchanged>,
42+
"course_id": <unchanged>,
43+
}
44+
"""
45+
log.info(
46+
"GradeEventContextEnricher running: user_id=%s, course_id=%s",
47+
str(user_id),
48+
str(course_id),
49+
)
50+
uuids = EnterpriseCourseEnrollment.get_enterprise_uuids_with_user_and_course(user_id, course_id)
51+
if uuids:
52+
context = copy.deepcopy(context) # create a copy to avoid altering the passed-in value.
53+
# Warning: Selecting the first element is not likely deterministic!
54+
context["enterprise_uuid"] = str(uuids[0])
55+
return {"context": context, "user_id": user_id, "course_id": course_id}

enterprise/settings/common.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
"fail_silently": False,
1717
"pipeline": ["enterprise.filters.dashboard.DashboardContextEnricher"],
1818
},
19+
"org.openedx.learning.grade.context.requested.v1": {
20+
"fail_silently": False,
21+
"pipeline": ["enterprise.filters.grades.GradeEventContextEnricher"],
22+
},
1923
}
2024

2125

tests/filters/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests for enterprise filter pipeline steps."""

tests/filters/test_grades.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""
2+
Tests for enterprise.filters.grades pipeline step.
3+
"""
4+
import uuid
5+
from unittest.mock import patch
6+
7+
from django.test import TestCase
8+
9+
from enterprise.filters.grades import GradeEventContextEnricher
10+
11+
12+
class TestGradeEventContextEnricher(TestCase):
13+
"""
14+
Tests for GradeEventContextEnricher pipeline step.
15+
"""
16+
17+
def _make_step(self):
18+
return GradeEventContextEnricher(
19+
filter_type="org.openedx.learning.grade.context.requested.v1",
20+
running_pipeline=[],
21+
)
22+
23+
@patch("enterprise.models.EnterpriseCourseEnrollment.get_enterprise_uuids_with_user_and_course")
24+
def test_enriches_context_when_enterprise_enrollment_found(self, mock_get_uuids):
25+
"""
26+
When an enterprise course enrollment exists, enterprise_uuid is added to context.
27+
"""
28+
enterprise_uuid = uuid.uuid4()
29+
mock_get_uuids.return_value = [enterprise_uuid]
30+
31+
step = self._make_step()
32+
context = {"org": "TestOrg", "course_id": "course-v1:org+course+run"}
33+
result = step.run_filter(context=context, user_id=7, course_id="course-v1:org+course+run")
34+
35+
assert result == {
36+
"context": {**context, "enterprise_uuid": str(enterprise_uuid)},
37+
"user_id": 7,
38+
"course_id": "course-v1:org+course+run",
39+
}
40+
mock_get_uuids.assert_called_once_with(7, "course-v1:org+course+run")
41+
42+
@patch("enterprise.models.EnterpriseCourseEnrollment.get_enterprise_uuids_with_user_and_course")
43+
def test_returns_unchanged_context_when_no_enterprise_enrollment(self, mock_get_uuids):
44+
"""
45+
When no enterprise course enrollment exists, context is returned unchanged.
46+
"""
47+
mock_get_uuids.return_value = []
48+
49+
step = self._make_step()
50+
context = {"org": "TestOrg"}
51+
result = step.run_filter(context=context, user_id=99, course_id="course-v1:org+course+run")
52+
53+
assert result == {
54+
"context": context,
55+
"user_id": 99,
56+
"course_id": "course-v1:org+course+run",
57+
}
58+
assert "enterprise_uuid" not in result["context"]
59+
60+
@patch("enterprise.models.EnterpriseCourseEnrollment.get_enterprise_uuids_with_user_and_course")
61+
def test_uses_first_uuid_when_multiple_enrollments(self, mock_get_uuids):
62+
"""
63+
When multiple enterprise enrollments exist, only the first UUID is used.
64+
"""
65+
first_uuid = uuid.uuid4()
66+
second_uuid = uuid.uuid4()
67+
mock_get_uuids.return_value = [first_uuid, second_uuid]
68+
69+
step = self._make_step()
70+
context = {}
71+
result = step.run_filter(context=context, user_id=1, course_id="course-v1:x+y+z")
72+
73+
assert result["context"]["enterprise_uuid"] == str(first_uuid)

0 commit comments

Comments
 (0)