From ddba068e93cb9aacac3a59f4fd7b1b6dba30bf9d Mon Sep 17 00:00:00 2001 From: Troy Sankey Date: Wed, 4 Mar 2026 09:58:30 -0800 Subject: [PATCH 1/5] feat: add pluggable override for learner home enterprise customer lookup ENT-11571 --- enterprise/overrides/learner_home.py | 35 +++++++++ .../test_overrides_learner_home.py | 76 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 enterprise/overrides/learner_home.py create mode 100644 tests/test_enterprise/test_overrides_learner_home.py diff --git a/enterprise/overrides/learner_home.py b/enterprise/overrides/learner_home.py new file mode 100644 index 0000000000..74812b7f0f --- /dev/null +++ b/enterprise/overrides/learner_home.py @@ -0,0 +1,35 @@ +""" +Pluggable override for the learner home enterprise customer lookup. +""" +import logging + +log = logging.getLogger(__name__) + + +def enterprise_get_enterprise_customer(prev_fn, user, request, is_masquerading): + """ + Return the enterprise customer dict for the given user, or None. + + This function overrides the default ``get_enterprise_customer`` implementation in + ``lms/djangoapps/learner_home/views.py`` via the pluggable override mechanism. + + Arguments: + prev_fn: the previous (default) implementation — not called because we fully replace it. + user: the Django User object. + request: the current HTTP request. + is_masquerading (bool): True when the request is a staff masquerade. + + Returns: + dict or None: enterprise customer data dict, or None if the user is not an + enterprise customer user. + """ + # Deferred imports — will be replaced with internal paths in epic 17. + from openedx.features.enterprise_support.api import ( # pylint: disable=import-outside-toplevel + enterprise_customer_from_session_or_learner_data, + get_enterprise_learner_data_from_db, + ) + + if is_masquerading: + learner_data = get_enterprise_learner_data_from_db(user) + return learner_data[0]['enterprise_customer'] if learner_data else None + return enterprise_customer_from_session_or_learner_data(request) diff --git a/tests/test_enterprise/test_overrides_learner_home.py b/tests/test_enterprise/test_overrides_learner_home.py new file mode 100644 index 0000000000..05a8f3f75d --- /dev/null +++ b/tests/test_enterprise/test_overrides_learner_home.py @@ -0,0 +1,76 @@ +""" +Tests for enterprise.overrides.learner_home pluggable override. +""" +from unittest import TestCase +from unittest.mock import MagicMock, patch + + +ENTERPRISE_SUPPORT_API_PATH = 'openedx.features.enterprise_support.api' + + +class TestEnterpriseGetEnterpriseCustomer(TestCase): + """ + Tests for enterprise_get_enterprise_customer override function. + """ + + def _call(self, user=None, request=None, is_masquerading=False): + """Import and call the override function.""" + # pylint: disable=import-outside-toplevel + from enterprise.overrides.learner_home import enterprise_get_enterprise_customer + prev_fn = MagicMock() + return enterprise_get_enterprise_customer(prev_fn, user, request, is_masquerading) + + def test_non_masquerading_delegates_to_session_api(self): + """ + When is_masquerading is False, should call + enterprise_customer_from_session_or_learner_data(request) and return its result. + """ + request = MagicMock() + user = MagicMock() + expected = {'uuid': 'test-uuid'} + mock_api = MagicMock() + mock_api.enterprise_customer_from_session_or_learner_data.return_value = expected + mock_api.get_enterprise_learner_data_from_db.return_value = [] + + with patch.dict('sys.modules', {ENTERPRISE_SUPPORT_API_PATH: mock_api}): + result = self._call(user=user, request=request, is_masquerading=False) + + mock_api.enterprise_customer_from_session_or_learner_data.assert_called_once_with(request) + mock_api.get_enterprise_learner_data_from_db.assert_not_called() + self.assertEqual(result, expected) + + def test_masquerading_returns_enterprise_customer_from_db(self): + """ + When is_masquerading is True and learner data exists, should return the + enterprise_customer from the first learner data entry. + """ + user = MagicMock() + request = MagicMock() + enterprise_customer = {'uuid': 'ec-uuid', 'name': 'Test Enterprise'} + mock_api = MagicMock() + mock_api.get_enterprise_learner_data_from_db.return_value = [ + {'enterprise_customer': enterprise_customer} + ] + + with patch.dict('sys.modules', {ENTERPRISE_SUPPORT_API_PATH: mock_api}): + result = self._call(user=user, request=request, is_masquerading=True) + + mock_api.get_enterprise_learner_data_from_db.assert_called_once_with(user) + mock_api.enterprise_customer_from_session_or_learner_data.assert_not_called() + self.assertEqual(result, enterprise_customer) + + def test_masquerading_returns_none_when_no_learner_data(self): + """ + When is_masquerading is True but no learner data exists, should return None. + """ + user = MagicMock() + request = MagicMock() + mock_api = MagicMock() + mock_api.get_enterprise_learner_data_from_db.return_value = [] + + with patch.dict('sys.modules', {ENTERPRISE_SUPPORT_API_PATH: mock_api}): + result = self._call(user=user, request=request, is_masquerading=True) + + mock_api.get_enterprise_learner_data_from_db.assert_called_once_with(user) + mock_api.enterprise_customer_from_session_or_learner_data.assert_not_called() + self.assertIsNone(result) From 968432a96e3379335c5dea8c081a28f29057a50c Mon Sep 17 00:00:00 2001 From: Marlon Keating Date: Thu, 2 Jul 2026 21:00:54 +0000 Subject: [PATCH 2/5] chore: quality fixes --- enterprise/overrides/learner_home.py | 20 +++++++++++++------ .../test_overrides_learner_home.py | 1 - 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/enterprise/overrides/learner_home.py b/enterprise/overrides/learner_home.py index 74812b7f0f..c6bdc90034 100644 --- a/enterprise/overrides/learner_home.py +++ b/enterprise/overrides/learner_home.py @@ -3,6 +3,16 @@ """ import logging +# Will be replaced with an internal path in epic 17. +try: + from openedx.features.enterprise_support.api import ( + enterprise_customer_from_session_or_learner_data, + get_enterprise_learner_data_from_db, + ) +except ImportError: + enterprise_customer_from_session_or_learner_data = None + get_enterprise_learner_data_from_db = None + log = logging.getLogger(__name__) @@ -14,7 +24,8 @@ def enterprise_get_enterprise_customer(prev_fn, user, request, is_masquerading): ``lms/djangoapps/learner_home/views.py`` via the pluggable override mechanism. Arguments: - prev_fn: the previous (default) implementation — not called because we fully replace it. + prev_fn: the previous (default) implementation, called when the enterprise + utility is unavailable. user: the Django User object. request: the current HTTP request. is_masquerading (bool): True when the request is a staff masquerade. @@ -23,11 +34,8 @@ def enterprise_get_enterprise_customer(prev_fn, user, request, is_masquerading): dict or None: enterprise customer data dict, or None if the user is not an enterprise customer user. """ - # Deferred imports — will be replaced with internal paths in epic 17. - from openedx.features.enterprise_support.api import ( # pylint: disable=import-outside-toplevel - enterprise_customer_from_session_or_learner_data, - get_enterprise_learner_data_from_db, - ) + if enterprise_customer_from_session_or_learner_data is None: + return prev_fn(user, request, is_masquerading) if is_masquerading: learner_data = get_enterprise_learner_data_from_db(user) diff --git a/tests/test_enterprise/test_overrides_learner_home.py b/tests/test_enterprise/test_overrides_learner_home.py index 05a8f3f75d..b67e19d383 100644 --- a/tests/test_enterprise/test_overrides_learner_home.py +++ b/tests/test_enterprise/test_overrides_learner_home.py @@ -4,7 +4,6 @@ from unittest import TestCase from unittest.mock import MagicMock, patch - ENTERPRISE_SUPPORT_API_PATH = 'openedx.features.enterprise_support.api' From 588a5f4075e72cf06f4d42cc912f8f1d17dfe223 Mon Sep 17 00:00:00 2001 From: Marlon Keating Date: Mon, 6 Jul 2026 19:26:31 +0000 Subject: [PATCH 3/5] docs: OVERRIDE_LEARNER_HOME_GET_ENTERPRISE_CUSTOMER TODO --- enterprise/overrides/learner_home.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/enterprise/overrides/learner_home.py b/enterprise/overrides/learner_home.py index c6bdc90034..84a852f6f4 100644 --- a/enterprise/overrides/learner_home.py +++ b/enterprise/overrides/learner_home.py @@ -1,5 +1,14 @@ """ Pluggable override for the learner home enterprise customer lookup. + +.. note:: + TODO: Wire this up to OVERRIDE_LEARNER_HOME_GET_ENTERPRISE_CUSTOMER once + edx-enterprise is a plugin (ENT-11584), before the plugin becomes optional. + + This override is **not yet wired up** to the + ``OVERRIDE_LEARNER_HOME_GET_ENTERPRISE_CUSTOMER`` setting. Registering it requires + edx-enterprise to be installable/importable as an openedx plugin, which is not yet + the case. """ import logging From aad269ec63fdbfa075fcaac902216f7873f4ba15 Mon Sep 17 00:00:00 2001 From: Marlon Keating Date: Fri, 10 Jul 2026 13:53:55 +0000 Subject: [PATCH 4/5] chore: bump version --- CHANGELOG.rst | 4 ++++ enterprise/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d5a4161705..14b5b6f0b5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,10 @@ Unreleased ---------- * nothing unreleased +[8.5.0] - 2026-07-10 +--------------------- +* feat: add pluggable override for learner home enterprise customer lookup + [8.4.0] - 2026-07-08 --------------------- * feat: add EnterpriseEnrollmentViewProcessor pipeline step for CourseEnrollmentViewStarted filter (ENT-11570) diff --git a/enterprise/__init__.py b/enterprise/__init__.py index 993f1957aa..3b3c611ace 100644 --- a/enterprise/__init__.py +++ b/enterprise/__init__.py @@ -2,4 +2,4 @@ Your project description goes here. """ -__version__ = "8.4.0" +__version__ = "8.5.0" From 65c1663baf9ad3c4822d065e2632aeae34e46d0e Mon Sep 17 00:00:00 2001 From: Marlon Keating Date: Fri, 10 Jul 2026 20:01:29 +0000 Subject: [PATCH 5/5] docs: Add type contract for get_enterprise_customer --- enterprise/overrides/learner_home.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/enterprise/overrides/learner_home.py b/enterprise/overrides/learner_home.py index 84a852f6f4..c0bff6e6ca 100644 --- a/enterprise/overrides/learner_home.py +++ b/enterprise/overrides/learner_home.py @@ -11,8 +11,11 @@ the case. """ import logging +from typing import TYPE_CHECKING, Optional, TypedDict -# Will be replaced with an internal path in epic 17. +from rest_framework.request import Request + +# Will be replaced with an internal path in ENT-11576. try: from openedx.features.enterprise_support.api import ( enterprise_customer_from_session_or_learner_data, @@ -22,10 +25,31 @@ enterprise_customer_from_session_or_learner_data = None get_enterprise_learner_data_from_db = None +if TYPE_CHECKING: + from django.contrib.auth.models import User + log = logging.getLogger(__name__) -def enterprise_get_enterprise_customer(prev_fn, user, request, is_masquerading): +class EnterpriseCustomerData(TypedDict): + """ + Required keys for the learner home enterprise dashboard. + + Mirrors ``lms.djangoapps.learner_home.views.EnterpriseCustomerData`` + """ + name: str + uuid: str + slug: str + auth_org_id: Optional[str] + enable_learner_portal: bool + + +def enterprise_get_enterprise_customer( + prev_fn, + user: "User", + request: Request, + is_masquerading: bool, +) -> Optional[EnterpriseCustomerData]: """ Return the enterprise customer dict for the given user, or None.