feat: enterprise learner portal access modal (#27736)
* feat: enterprise learner portal access modal * fix: quality * fix: remove log.info
This commit is contained in:
@@ -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,
|
||||
|
||||
126
lms/static/js/learner_dashboard/EnterpriseLearnerPortalModal.jsx
Normal file
126
lms/static/js/learner_dashboard/EnterpriseLearnerPortalModal.jsx
Normal file
@@ -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 (
|
||||
<div
|
||||
role="dialog"
|
||||
className="modal-wrapper d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<FocusLock>
|
||||
<div
|
||||
className="modal-content p-4 bg-white"
|
||||
ref={(node) => { this.modalRef = node; }}
|
||||
>
|
||||
<div className="mb-3 font-weight-bold">
|
||||
{StringUtils.interpolate(
|
||||
gettext('You have access to the {enterpriseName} dashboard'),
|
||||
{
|
||||
enterpriseName: this.props.enterpriseCustomerName,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<p>
|
||||
{StringUtils.interpolate(
|
||||
gettext('To access the courses available to you through {enterpriseName}, visit the {enterpriseName} dashboard.'),
|
||||
{
|
||||
enterpriseName: this.props.enterpriseCustomerName,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<div className="mt-4 d-flex align-content-center justify-content-end">
|
||||
<button
|
||||
className="btn-link mr-3"
|
||||
onClick={() => this.closeModal()}
|
||||
>
|
||||
{gettext('Cancel')}
|
||||
</button>
|
||||
<AutoFocusInside>
|
||||
<a
|
||||
href={this.getLearnerPortalUrl()}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{gettext('Go to dashboard')}
|
||||
</a>
|
||||
</AutoFocusInside>
|
||||
</div>
|
||||
</div>
|
||||
</FocusLock>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { EnterpriseLearnerPortalModal };
|
||||
@@ -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("<a href='{learner_portal_url}'>").format(
|
||||
learner_portal_url=learner_portal_url,
|
||||
),
|
||||
link_end=HTML("</a>"),
|
||||
bold_start=HTML("<b>"),
|
||||
bold_end=HTML("</b>"),
|
||||
screen_reader_start=HTML("<span class='sr-only'>"),
|
||||
screen_reader_end=HTML("</span>"),
|
||||
)
|
||||
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("<b>"),
|
||||
bold_end=HTML("</b>"),
|
||||
link_start=HTML(f"<a href='{learner_portal_url}'>"),
|
||||
link_end=HTML("</a>"),
|
||||
)
|
||||
|
||||
|
||||
@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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user