feat: enterprise learner portal access modal (#27736)

* feat: enterprise learner portal access modal

* fix: quality

* fix: remove log.info
This commit is contained in:
Adam Stankiewicz
2021-05-27 15:31:27 -04:00
committed by GitHub
parent a131d63608
commit 31f66a4f2c
5 changed files with 223 additions and 57 deletions

View File

@@ -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,

View 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 };

View File

@@ -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

View File

@@ -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):

View File

@@ -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',