diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py
index c6d31c588a..34bb107243 100644
--- a/common/djangoapps/student/views/dashboard.py
+++ b/common/djangoapps/student/views/dashboard.py
@@ -44,7 +44,7 @@ from openedx.core.djangoapps.util.maintenance_banner import add_maintenance_bann
from openedx.core.djangolib.markup import HTML, Text
from openedx.features.enterprise_support.api import (
get_dashboard_consent_notification,
- get_enterprise_learner_portal_enabled_message
+ get_enterprise_learner_portal_context,
)
from common.djangoapps.student.api import COURSE_DASHBOARD_PLUGIN_VIEW_NAME
from common.djangoapps.student.helpers import cert_info, check_verify_status_by_course, get_resume_urls_for_enrollments
@@ -576,9 +576,6 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
enterprise_message = get_dashboard_consent_notification(request, user, course_enrollments)
- # Display a message guiding the user to their Enterprise's Learner Portal if enabled
- enterprise_learner_portal_enabled_message = get_enterprise_learner_portal_enabled_message(request)
-
recovery_email_message = recovery_email_activation_message = None
if is_secondary_email_feature_enabled():
try:
@@ -605,11 +602,9 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
)
)
-
-# Disable lookup of Enterprise consent_required_course due to ENT-727
+ # Disable lookup of Enterprise consent_required_course due to ENT-727
# Will re-enable after fixing WL-1315
consent_required_courses = set()
- enterprise_customer_name = None
# Account activation message
account_activation_messages = [
@@ -752,7 +747,6 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
'programs_data': programs_data,
'enterprise_message': enterprise_message,
'consent_required_courses': consent_required_courses,
- 'enterprise_customer_name': enterprise_customer_name,
'enrollment_message': enrollment_message,
'redirect_message': Text(redirect_message),
'account_activation_messages': account_activation_messages,
@@ -796,13 +790,16 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
'empty_dashboard_message': empty_dashboard_message,
'recovery_email_message': recovery_email_message,
'recovery_email_activation_message': recovery_email_activation_message,
- 'enterprise_learner_portal_enabled_message': enterprise_learner_portal_enabled_message,
'show_load_all_courses_link': show_load_all_courses_link(user, course_limit, course_enrollments),
# TODO START: clean up as part of REVEM-199 (START)
'course_info': get_dashboard_course_info(user, course_enrollments),
# TODO START: clean up as part of REVEM-199 (END)
}
+ # Include enterprise learner portal metadata and messaging
+ enterprise_learner_portal_context = get_enterprise_learner_portal_context(request)
+ context.update(enterprise_learner_portal_context)
+
context_from_plugins = get_plugins_view_context(
ProjectType.LMS,
COURSE_DASHBOARD_PLUGIN_VIEW_NAME,
diff --git a/lms/static/js/learner_dashboard/EnterpriseLearnerPortalModal.jsx b/lms/static/js/learner_dashboard/EnterpriseLearnerPortalModal.jsx
new file mode 100644
index 0000000000..884718855f
--- /dev/null
+++ b/lms/static/js/learner_dashboard/EnterpriseLearnerPortalModal.jsx
@@ -0,0 +1,126 @@
+/* global gettext */
+import React from 'react';
+import FocusLock, { AutoFocusInside } from 'react-focus-lock';
+import StringUtils from 'edx-ui-toolkit/js/utils/string-utils';
+
+class EnterpriseLearnerPortalModal extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isModalOpen: false,
+ };
+
+ this.openModal = this.openModal.bind(this);
+ this.closeModal = this.closeModal.bind(this);
+ this.handleClick = this.handleClick.bind(this);
+ }
+
+ componentDidMount() {
+ const storageKey = `enterprise_learner_portal_modal__${this.props.enterpriseCustomerUUID}`;
+ const hasViewedModal = window.sessionStorage.getItem(storageKey);
+ if (!hasViewedModal) {
+ this.openModal();
+ document.addEventListener('mousedown', this.handleClick, false);
+ window.sessionStorage.setItem(storageKey, true);
+ }
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (this.state.isModalOpen !== prevState.isModalOpen) {
+ if (this.state.isModalOpen) {
+ // add a class here to prevent scrolling on anything that is not the modal
+ document.body.classList.add('modal-open');
+ } else {
+ // remove the class to allow the dashboard content to scroll
+ document.body.classList.remove('modal-open');
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ // remove the class to allow the dashboard content to scroll
+ document.body.classList.remove('modal-open');
+ document.removeEventListener('mousedown', this.handleClick, false);
+ }
+
+ handleClick(e) {
+ if (this.modalRef && this.modalRef.contains(e.target)) {
+ // click is inside modal, don't close it
+ return;
+ }
+
+ this.closeModal();
+ }
+
+ closeModal() {
+ this.setState({
+ isModalOpen: false,
+ });
+ }
+
+ openModal() {
+ this.setState({
+ isModalOpen: true,
+ });
+ }
+
+ getLearnerPortalUrl() {
+ const baseUrlWithSlug = `${this.props.enterpriseLearnerPortalBaseUrl}/${this.props.enterpriseCustomerSlug}`;
+ return `${baseUrlWithSlug}?utm_source=lms_dashboard_modal`;
+ }
+
+ render() {
+ if (!this.state.isModalOpen) {
+ return null;
+ }
+
+ return (
+
+
+ { this.modalRef = node; }}
+ >
+
+ {StringUtils.interpolate(
+ gettext('You have access to the {enterpriseName} dashboard'),
+ {
+ enterpriseName: this.props.enterpriseCustomerName,
+ }
+ )}
+
+
+ {StringUtils.interpolate(
+ gettext('To access the courses available to you through {enterpriseName}, visit the {enterpriseName} dashboard.'),
+ {
+ enterpriseName: this.props.enterpriseCustomerName,
+ }
+ )}
+
+
+
+
+
+ );
+ }
+}
+
+export { EnterpriseLearnerPortalModal };
diff --git a/openedx/features/enterprise_support/api.py b/openedx/features/enterprise_support/api.py
index f9a2934fb7..ba53e77885 100644
--- a/openedx/features/enterprise_support/api.py
+++ b/openedx/features/enterprise_support/api.py
@@ -787,13 +787,12 @@ def get_enterprise_learner_data_from_db(user):
@enterprise_is_enabled()
-def get_enterprise_learner_portal_enabled_message(request):
+def enterprise_customer_from_session_or_learner_data(request):
"""
- Returns message to be displayed in dashboard if the user is linked to an Enterprise with the Learner Portal enabled.
+ Returns an Enterprise Customer for the authenticated user.
- Note: request.session[ENTERPRISE_CUSTOMER_KEY_NAME] will be used in case the user is linked to
- multiple Enterprises. Otherwise, it won't exist and the Enterprise Learner data
- will be used. If that doesn't exist return None.
+ Retrieves customer from session by default. If _CACHE_MISS, retrieve customer using
+ learner data from the DB and add customer data to the session.
Args:
request: request made to the LMS dashboard
@@ -812,31 +811,73 @@ def get_enterprise_learner_portal_enabled_message(request):
add_enterprise_customer_to_session(request, enterprise_customer)
if enterprise_customer:
cache_enterprise(enterprise_customer)
+ return enterprise_customer
+
+@enterprise_is_enabled()
+def get_enterprise_learner_portal_enabled_message(enterprise_customer):
+ """
+ Returns message to be displayed in dashboard if the user is linked to an Enterprise with the Learner Portal enabled.
+ Note: request.session[ENTERPRISE_CUSTOMER_KEY_NAME] will be used in case the user is linked to
+ multiple Enterprises. Otherwise, it won't exist and the Enterprise Learner data
+ will be used. If that doesn't exist return None.
+ Args:
+ enterprise_customer: EnterpriseCustomer object
+ """
if not enterprise_customer:
return None
- if enterprise_customer.get('enable_learner_portal', False):
- learner_portal_url = settings.ENTERPRISE_LEARNER_PORTAL_BASE_URL + '/' + enterprise_customer['slug']
- return Text(_(
- "Your organization {bold_start}{enterprise_customer_name}{bold_end} uses a custom dashboard for learning. "
- "{link_start}Click here {screen_reader_start}for your {enterprise_customer_name} dashboard,"
- "{screen_reader_end}{link_end} to continue in that experience."
- )).format(
- enterprise_customer_name=enterprise_customer['name'],
- link_start=HTML("").format(
- learner_portal_url=learner_portal_url,
- ),
- link_end=HTML(""),
- bold_start=HTML(""),
- bold_end=HTML(""),
- screen_reader_start=HTML(""),
- screen_reader_end=HTML(""),
- )
- else:
+ if not enterprise_customer.get('enable_learner_portal', False):
return None
+ learner_portal_url = "{base_url}/{slug}?utm_source=lms_dashboard_banner".format(
+ base_url=settings.ENTERPRISE_LEARNER_PORTAL_BASE_URL,
+ slug=enterprise_customer['slug']
+ )
+ return Text(_(
+ "You have access to the {bold_start}{enterprise_name}{bold_end} dashboard. "
+ "To access the courses available to you through {enterprise_name}, "
+ "{link_start}visit the {enterprise_name} dashboard{link_end}."
+ )).format(
+ enterprise_name=enterprise_customer['name'],
+ bold_start=HTML(""),
+ bold_end=HTML(""),
+ link_start=HTML(f""),
+ link_end=HTML(""),
+ )
+
+
+@enterprise_is_enabled(otherwise={})
+def get_enterprise_learner_portal_context(request):
+ """
+ Determines a selected enterprise customer from session or learner data from the DB.
+
+ Arguments:
+ request: A request object.
+
+ Returns:
+ dict: A dictionary representing the necessary metadata and messaging about an Enterprise Learner Portal,
+ used in the dashboard.html template.
+ """
+ context = {}
+ enterprise_customer = enterprise_customer_from_session_or_learner_data(request)
+ if not enterprise_customer:
+ return context
+
+ enterprise_learner_portal_enabled_message = get_enterprise_learner_portal_enabled_message(enterprise_customer)
+ context.update({
+ 'enterprise_customer_name': enterprise_customer.get('name'),
+ 'enterprise_customer_slug': enterprise_customer.get('slug'),
+ 'enterprise_customer_learner_portal_enabled': enterprise_customer.get('enable_learner_portal', False),
+ 'enterprise_customer_uuid': enterprise_customer.get('uuid'),
+ 'enterprise_learner_portal_base_url': settings.ENTERPRISE_LEARNER_PORTAL_BASE_URL,
+ 'enterprise_learner_portal_enabled_message': enterprise_learner_portal_enabled_message,
+ })
+ return context
+
+
+@enterprise_is_enabled()
def get_consent_notification_data(enterprise_customer):
"""
Returns the consent notification data from DataSharingConsentPage modal
diff --git a/openedx/features/enterprise_support/tests/test_api.py b/openedx/features/enterprise_support/tests/test_api.py
index 4307d3be3c..fcb431b54c 100644
--- a/openedx/features/enterprise_support/tests/test_api.py
+++ b/openedx/features/enterprise_support/tests/test_api.py
@@ -35,6 +35,7 @@ from openedx.features.enterprise_support.api import (
data_sharing_consent_required,
enterprise_customer_for_request,
enterprise_customer_from_api,
+ enterprise_customer_from_session_or_learner_data,
enterprise_customer_uuid_for_request,
enterprise_enabled,
get_consent_notification_data,
@@ -777,26 +778,26 @@ class TestEnterpriseApi(EnterpriseServiceMockMixin, CacheIsolationTestCase):
]
@mock.patch('openedx.features.enterprise_support.api.get_enterprise_learner_data_from_db')
- def test_enterprise_learner_portal_message_cache_miss_no_customer(self, mock_learner_data_from_db):
+ def test_enterprise_customer_from_session_or_db_cache_miss_no_customer(self, mock_learner_data_from_db):
"""
When no customer data exists in the request session _and_
- no customer is associated with the requesting user, then ``get_enterprise_learner_portal_enabled_message()``
+ no customer is associated with the requesting user, then ``enterprise_customer_from_session_or_learner_data()``
should return None.
"""
mock_request = mock.Mock(session={})
mock_learner_data_from_db.return_value = None
- actual_result = get_enterprise_learner_portal_enabled_message(mock_request)
+ actual_result = enterprise_customer_from_session_or_learner_data(mock_request)
assert actual_result is None
mock_learner_data_from_db.assert_called_once_with(mock_request.user)
@mock.patch('openedx.features.enterprise_support.api.get_enterprise_learner_data_from_db')
@override_settings(ENTERPRISE_LEARNER_PORTAL_BASE_URL='http://localhost')
- def test_enterprise_learner_portal_message_cache_miss_customer_exists(self, mock_learner_data_from_db):
+ def test_enterprise_customer_from_session_or_db_cache_miss_customer_exists(self, mock_learner_data_from_db):
"""
When no customer data exists in the request session but a
- customer is associated with the requesting user, then ``get_enterprise_learner_portal_enabled_message()``
- should return an appropriate message for that customer.
+ customer is associated with the requesting user, then ``enterprise_customer_from_session_or_learner_data()``
+ should return the customer metadata.
"""
mock_request = mock.Mock(session={})
mock_enterprise_customer = {
@@ -811,37 +812,33 @@ class TestEnterpriseApi(EnterpriseServiceMockMixin, CacheIsolationTestCase):
},
]
- actual_result = get_enterprise_learner_portal_enabled_message(mock_request)
- assert 'custom dashboard for learning' in actual_result
- assert 'Best Corp' in actual_result
+ actual_result = enterprise_customer_from_session_or_learner_data(mock_request)
+ assert actual_result['uuid'] == mock_enterprise_customer['uuid']
mock_learner_data_from_db.assert_called_once_with(mock_request.user)
# assert we cached the enterprise customer data in the request session after fetching it
assert mock_request.session.get(ENTERPRISE_CUSTOMER_KEY_NAME) == mock_enterprise_customer
@mock.patch('openedx.features.enterprise_support.api.get_enterprise_learner_data_from_db')
- def test_enterprise_learner_portal_message_cache_hit_no_customer(self, mock_learner_data_from_db):
+ def test_enterprise_customer_from_session_or_db_cache_hit_no_customer(self, mock_learner_data_from_db):
"""
When customer data exists in the request session but it's null/empty,
- then ``get_enterprise_learner_portal_enabled_message()`` should return None.
+ then ``enterprise_customer_from_session_or_learner_data()`` should return None.
"""
mock_request = mock.Mock(session={
ENTERPRISE_CUSTOMER_KEY_NAME: None,
})
- actual_result = get_enterprise_learner_portal_enabled_message(mock_request)
+ actual_result = enterprise_customer_from_session_or_learner_data(mock_request)
assert actual_result is None
assert not mock_learner_data_from_db.called
@ddt.data(True, False)
- @mock.patch('openedx.features.enterprise_support.api.get_enterprise_learner_data_from_db')
@override_settings(ENTERPRISE_LEARNER_PORTAL_BASE_URL='http://localhost')
- def test_enterprise_learner_portal_message_cache_hit_customer_exists(
- self, enable_learner_portal, mock_learner_data_from_db
- ):
+ def test_enterprise_learner_portal_message_customer_exists(self, enable_learner_portal):
"""
- When customer data exists in the request session and it's a non-empty customer,
- then ``get_enterprise_learner_portal_enabled_message()`` should return
- an appropriate message for that customer.
+ When an enterprise customer exists with learner portal enabled, then
+ ``get_enterprise_learner_portal_enabled_message()`` should return an appropriate message
+ for that customer.
"""
mock_enterprise_customer = {
'uuid': 'some-uuid',
@@ -849,17 +846,21 @@ class TestEnterpriseApi(EnterpriseServiceMockMixin, CacheIsolationTestCase):
'enable_learner_portal': enable_learner_portal,
'slug': 'best-corp',
}
- mock_request = mock.Mock(session={
- ENTERPRISE_CUSTOMER_KEY_NAME: mock_enterprise_customer,
- })
- actual_result = get_enterprise_learner_portal_enabled_message(mock_request)
+ actual_result = get_enterprise_learner_portal_enabled_message(mock_enterprise_customer)
if not enable_learner_portal:
assert actual_result is None
else:
- assert 'custom dashboard for learning' in actual_result
+ assert 'To access the courses available to you through' in actual_result
assert 'Best Corp' in actual_result
- assert not mock_learner_data_from_db.called
+
+ def test_enterprise_learner_portal_message_no_customer(self):
+ """
+ When an enterprise customer does not exists, then
+ ``get_enterprise_learner_portal_enabled_message()`` should return None.
+ """
+ actual_result = get_enterprise_learner_portal_enabled_message(None)
+ assert actual_result is None
@mock.patch('openedx.features.enterprise_support.api.get_partial_pipeline', return_value=None)
def test_customer_uuid_for_request_sso_provider_id_customer_exists(self, mock_partial_pipeline):
diff --git a/webpack.common.config.js b/webpack.common.config.js
index c7d7a82118..0f76269a88 100644
--- a/webpack.common.config.js
+++ b/webpack.common.config.js
@@ -96,6 +96,7 @@ module.exports = Merge.smart({
DemographicsCollectionBanner: './lms/static/js/demographics_collection/DemographicsCollectionBanner.jsx',
DemographicsCollectionModal: './lms/static/js/demographics_collection/DemographicsCollectionModal.jsx',
AxiosJwtTokenService: './lms/static/js/jwt_auth/AxiosJwtTokenService.js',
+ EnterpriseLearnerPortalModal: './lms/static/js/learner_dashboard/EnterpriseLearnerPortalModal.jsx',
// Learner Dashboard
EntitlementFactory: './lms/static/js/learner_dashboard/course_entitlement_factory.js',