From e581eebf3a5bf6397a622369db48c70fc9d7ec09 Mon Sep 17 00:00:00 2001 From: Marlon Keating Date: Fri, 31 Jul 2026 19:45:38 +0000 Subject: [PATCH] feat: Add plugin hook for activation email context, and remove enterprise context A new OpenEdX filter AccountActivationEmailContextGenerated is called (in place of custom enterprise logic) to allow plugins more flexibility in customizing the account activation email via theme-provided custom email templates. Relatedly, this removes in-platform injection of the `is_enterprise_learner` key into the activation email context. This can now be accomplished via plugins which implement a pipeline step for the newly added filter. ENT-11816 --- .../djangoapps/student/tests/test_filters.py | 77 ++++++++++++++++++- common/djangoapps/student/views/management.py | 8 +- .../djangoapps/user_authn/tests/test_tasks.py | 1 - requirements/constraints.txt | 2 +- requirements/edx/base.txt | 4 +- requirements/edx/development.txt | 4 +- requirements/edx/doc.txt | 4 +- requirements/edx/testing.txt | 4 +- 8 files changed, 90 insertions(+), 14 deletions(-) diff --git a/common/djangoapps/student/tests/test_filters.py b/common/djangoapps/student/tests/test_filters.py index bf79ed7ae402..c906ce2fdcea 100644 --- a/common/djangoapps/student/tests/test_filters.py +++ b/common/djangoapps/student/tests/test_filters.py @@ -6,13 +6,19 @@ from django.test import override_settings from django.urls import reverse from openedx_filters import PipelineStep -from openedx_filters.learning.filters import DashboardRenderStarted, CourseEnrollmentStarted, CourseUnenrollmentStarted +from openedx_filters.learning.filters import CourseEnrollmentStarted, CourseUnenrollmentStarted, DashboardRenderStarted from rest_framework import status from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory -from common.djangoapps.student.models import CourseEnrollment, EnrollmentNotAllowed, UnenrollmentNotAllowed +from common.djangoapps.student.models import ( + CourseEnrollment, + EnrollmentNotAllowed, + Registration, + UnenrollmentNotAllowed, +) from common.djangoapps.student.tests.factories import UserFactory, UserProfileFactory +from common.djangoapps.student.views.management import compose_activation_email from openedx.core.djangolib.testing.utils import skip_unless_lms @@ -111,6 +117,18 @@ def run_filter(self, context, template_name): # pylint: disable=arguments-diffe ) +class ActivationEmailWaldoEnricher(PipelineStep): + """ + Test pipeline step for the AccountActivationEmailContextGenerated filter + which adds a `show_waldo` context key. + """ + + def run_filter(self, user, message_context): # pylint: disable=arguments-differ + """Pipeline step that stamps a fake show_waldo flag onto the activation email context.""" + message_context["show_waldo"] = True + return {"user": user, "message_context": message_context} + + @skip_unless_lms class EnrollmentFiltersTest(ModuleStoreTestCase): """ @@ -464,3 +482,58 @@ def test_dashboard_render_without_filter_config(self): self.assertContains(response, self.first_course.id) self.assertContains(response, self.second_course.id) + + +@skip_unless_lms +class AccountActivationEmailFiltersTest(ModuleStoreTestCase): + """ + Tests for the Open edX Filters associated with the account activation email context. + + This class guarantees that the following filter is triggered when the activation email + context is generated: + - AccountActivationEmailContextGenerated + """ + + def setUp(self): # pylint: disable=arguments-differ + super().setUp() + self.user = UserFactory() + self.registration = Registration() + self.registration.register(self.user) + self.registration.save() + + @override_settings( + OPEN_EDX_FILTERS_CONFIG={ + "org.openedx.authentication.account_activation.email.context.generated.v1": { + "pipeline": [ + "common.djangoapps.student.tests.test_filters.ActivationEmailWaldoEnricher", + ], + "fail_silently": False, + }, + }, + ) + def test_activation_email_context_generated_filter_executed(self): + """ + Test whether the activation email context filter is triggered before the + activation email message context is finalized. + + Expected result: + - AccountActivationEmailContextGenerated is triggered and executes + ActivationEmailWaldoEnricher. + - The composed message's context contains the pipeline step's modification. + """ + message = compose_activation_email(self.user, self.registration) + + assert message.context["show_waldo"] is True + + @override_settings(OPEN_EDX_FILTERS_CONFIG={}) + def test_activation_email_context_generated_without_filter_config(self): + """ + Test that compose_activation_email succeeds with no pipeline steps configured. + + Expected result: + - AccountActivationEmailContextGenerated executes a noop (empty pipeline). + - No 'show_waldo' key is injected into the message context. + """ + message = compose_activation_email(self.user, self.registration) + + assert "show_waldo" not in message.context diff --git a/common/djangoapps/student/views/management.py b/common/djangoapps/student/views/management.py index d45ba1001015..86e36b4991a3 100644 --- a/common/djangoapps/student/views/management.py +++ b/common/djangoapps/student/views/management.py @@ -36,6 +36,7 @@ # Note that this lives in LMS, so this dependency should be refactored. from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey +from openedx_filters.authentication.filters import AccountActivationEmailContextGenerated from rest_framework.decorators import api_view, authentication_classes, permission_classes from rest_framework.permissions import IsAuthenticated @@ -98,7 +99,6 @@ from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser from openedx.features.course_experience.url_helpers import make_learning_mfe_courseware_url from openedx.features.discounts.applicability import FIRST_PURCHASE_DISCOUNT_OVERRIDE_FLAG -from openedx.features.enterprise_support.utils import is_enterprise_learner from common.djangoapps.util.db import outer_atomic from common.djangoapps.util.json_request import JsonResponse from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order @@ -222,7 +222,6 @@ def compose_activation_email( message_context = generate_activation_email_context(user, user_registration) message_context.update({ 'confirm_activation_link': _get_activation_confirmation_link(message_context['key'], redirect_url), - 'is_enterprise_learner': is_enterprise_learner(user), 'is_first_purchase_discount_overridden': FIRST_PURCHASE_DISCOUNT_OVERRIDE_FLAG.is_enabled(), 'route_enabled': route_enabled, 'routed_user': user.username, @@ -231,6 +230,11 @@ def compose_activation_email( 'registration_flow': registration_flow, 'show_auto_generated_username': show_auto_generated_username(user.username), }) + # .. filter_implemented_name: AccountActivationEmailContextGenerated + # .. filter_type: org.openedx.authentication.account_activation.email.context.generated.v1 + __, message_context = AccountActivationEmailContextGenerated.run_filter( + user=user, message_context=message_context, + ) if route_enabled: dest_addr = settings.FEATURES['REROUTE_ACTIVATION_EMAIL'] diff --git a/openedx/core/djangoapps/user_authn/tests/test_tasks.py b/openedx/core/djangoapps/user_authn/tests/test_tasks.py index 5103343a0879..60bb56133fc4 100644 --- a/openedx/core/djangoapps/user_authn/tests/test_tasks.py +++ b/openedx/core/djangoapps/user_authn/tests/test_tasks.py @@ -44,7 +44,6 @@ def test_ComposeEmail(self): assert self.msg.context['routed_user_email'] == self.student.email assert self.msg.context['routed_profile_name'] == '' assert self.msg.context['registration_flow'] is False - assert self.msg.context['is_enterprise_learner'] is False assert self.msg.context['is_first_purchase_discount_overridden'] is False @mock.patch('time.sleep', mock.Mock(return_value=None)) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index c53149d615fb..e7b149450fc7 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -42,7 +42,7 @@ django-stubs<6 # The team that owns this package will manually bump this package rather than having it pulled in automatically. # This is to allow them to better control its deployment and to do it in a process that works better # for them. -edx-enterprise==8.11.0 +edx-enterprise==8.14.0 # Date: 2023-07-26 # Our legacy Sass code is incompatible with anything except this ancient libsass version. diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 3e2ecd6deee4..2d32f1806fa0 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -473,7 +473,7 @@ edx-drf-extensions==10.6.0 # edxval # enterprise-integrated-channels # openedx-learning -edx-enterprise==8.11.0 +edx-enterprise==8.14.0 # via # -c requirements/constraints.txt # -r requirements/edx/kernel.in @@ -829,7 +829,7 @@ openedx-events==10.5.0 # edx-name-affirmation # event-tracking # ora2 -openedx-filters==3.11.0 +openedx-filters==3.14.0 # via # -r requirements/edx/kernel.in # edx-enterprise diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index c6ee3e6a5b6e..e858cf0f8617 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -747,7 +747,7 @@ edx-drf-extensions==10.6.0 # edxval # enterprise-integrated-channels # openedx-learning -edx-enterprise==8.11.0 +edx-enterprise==8.14.0 # via # -c requirements/constraints.txt # -r requirements/edx/doc.txt @@ -1375,7 +1375,7 @@ openedx-events==10.5.0 # edx-name-affirmation # event-tracking # ora2 -openedx-filters==3.11.0 +openedx-filters==3.14.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 168917cea36f..8c99e6d59059 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -557,7 +557,7 @@ edx-drf-extensions==10.6.0 # edxval # enterprise-integrated-channels # openedx-learning -edx-enterprise==8.11.0 +edx-enterprise==8.14.0 # via # -c requirements/constraints.txt # -r requirements/edx/base.txt @@ -1001,7 +1001,7 @@ openedx-events==10.5.0 # edx-name-affirmation # event-tracking # ora2 -openedx-filters==3.11.0 +openedx-filters==3.14.0 # via # -r requirements/edx/base.txt # edx-enterprise diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index daca6a0ec553..fc1b3bebe13a 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -578,7 +578,7 @@ edx-drf-extensions==10.6.0 # edxval # enterprise-integrated-channels # openedx-learning -edx-enterprise==8.11.0 +edx-enterprise==8.14.0 # via # -c requirements/constraints.txt # -r requirements/edx/base.txt @@ -1046,7 +1046,7 @@ openedx-events==10.5.0 # edx-name-affirmation # event-tracking # ora2 -openedx-filters==3.11.0 +openedx-filters==3.14.0 # via # -r requirements/edx/base.txt # edx-enterprise