diff --git a/cms/envs/common.py b/cms/envs/common.py index d0ab304b8337a500a30df0dc06da027189ae31d0..51cef45c243c46bf18c2439bd939d21f53280188 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -2415,3 +2415,7 @@ DEFAULT_EMAIL_LOGO_URL = 'https://edx-cdn.org/v3/default/logo.png' ############## Settings for course import olx validation ############################ COURSE_OLX_VALIDATION_STAGE = 1 COURSE_OLX_VALIDATION_IGNORE_LIST = None + +################# show account activate cta after register ######################## +SHOW_ACTIVATE_CTA_POPUP_COOKIE_NAME = 'show-account-activation-popup' +SHOW_ACCOUNT_ACTIVATION_CTA = False diff --git a/cms/envs/production.py b/cms/envs/production.py index 679cc19295c889101433db72716b7e26ad874f24..1ce1a223aeb09698cc1dbcca1ed6610160201b2c 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -608,3 +608,6 @@ COURSE_OLX_VALIDATION_IGNORE_LIST = ENV_TOKENS.get( 'COURSE_OLX_VALIDATION_IGNORE_LIST', COURSE_OLX_VALIDATION_IGNORE_LIST ) + +################# show account activate cta after register ######################## +SHOW_ACCOUNT_ACTIVATION_CTA = ENV_TOKENS.get('SHOW_ACCOUNT_ACTIVATION_CTA', SHOW_ACCOUNT_ACTIVATION_CTA) diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index 34bb107243b5f07e05cc46c4651c37783dd53745..779a1320ec866beb99310e0b3c4555eb6916a047 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -742,6 +742,8 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem enr for enr in course_enrollments if entitlement.enrollment_course_run.course_id != enr.course_id ] + show_account_activation_popup = request.COOKIES.get(settings.SHOW_ACTIVATE_CTA_POPUP_COOKIE_NAME, None) + context = { 'urls': urls, 'programs_data': programs_data, @@ -771,6 +773,7 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem 'verification_errors': verification_errors, 'denied_banner': denied_banner, 'billing_email': settings.PAYMENT_SUPPORT_EMAIL, + 'show_account_activation_popup': show_account_activation_popup, 'user': user, 'logout_url': reverse('logout'), 'platform_name': platform_name, @@ -830,4 +833,12 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem 'resume_button_urls': resume_button_urls }) - return render_to_response('dashboard.html', context) + response = render_to_response('dashboard.html', context) + if show_account_activation_popup: + response.delete_cookie( + settings.SHOW_ACTIVATE_CTA_POPUP_COOKIE_NAME, + domain=settings.SESSION_COOKIE_DOMAIN, + path='/', + ) + + return response diff --git a/lms/envs/common.py b/lms/envs/common.py index 7f585c3aec7539136be57a1370ae2c2bc119671c..f55fa1c59d7430c40845f69dfd59cd9b499060b3 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -4720,3 +4720,13 @@ DEFAULT_EMAIL_LOGO_URL = 'https://edx-cdn.org/v3/default/logo.png' ################# Settings for olx validation. ################# COURSE_OLX_VALIDATION_STAGE = 1 COURSE_OLX_VALIDATION_IGNORE_LIST = None + +################# show account activate cta after register ######################## +SHOW_ACTIVATE_CTA_POPUP_COOKIE_NAME = 'show-account-activation-popup' +# .. toggle_name: SOME_FEATURE_NAME +# .. toggle_implementation: DjangoSetting +# .. toggle_default: False +# .. toggle_description: Flag would be used to show account activation popup after the registration +# .. toggle_use_cases: open_edx +# .. toggle_tickets: https://github.com/edx/edx-platform/pull/27661 +SHOW_ACCOUNT_ACTIVATION_CTA = False diff --git a/lms/envs/production.py b/lms/envs/production.py index 86dff145ed00523810088268ebca5e8767981f75..70bfa038ad17dbb3cd3f1cc97ede35f8837fea49 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1056,3 +1056,6 @@ COURSE_OLX_VALIDATION_IGNORE_LIST = ENV_TOKENS.get( 'COURSE_OLX_VALIDATION_IGNORE_LIST', COURSE_OLX_VALIDATION_IGNORE_LIST ) + +################# show account activate cta after register ######################## +SHOW_ACCOUNT_ACTIVATION_CTA = ENV_TOKENS.get('SHOW_ACCOUNT_ACTIVATION_CTA', SHOW_ACCOUNT_ACTIVATION_CTA) diff --git a/lms/static/js/dashboard/legacy.js b/lms/static/js/dashboard/legacy.js index 966dc4c93d7ed5a71c2fa1cf06bf4ea4434f5e78..2ad6ac76c25756e28d9f8ed5d73ea4b7e7f95507 100644 --- a/lms/static/js/dashboard/legacy.js +++ b/lms/static/js/dashboard/legacy.js @@ -228,6 +228,50 @@ return false; }); + $('#send_cta_email').click(function(e) { + $.ajax({ + type: 'POST', + url: urls.sendAccountActivationEmail, + data: $(this).serializeArray(), + success: function() { + $('#activate-account-modal p svg').remove(); + // xss-lint: disable=javascript-jquery-append + $('#activate-account-modal p').append( + // xss-lint: disable=javascript-concat-html + '<svg style="vertical-align:bottom" width="20" height="20"' + + // xss-lint: disable=javascript-concat-html + 'viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">\n' + + // xss-lint: disable=javascript-concat-html + '<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" fill="#178253"/>\n' + + '</svg>' + ); + } + }); + e.preventDefault(); + $('#activate-account-modal p svg').remove(); + // xss-lint: disable=javascript-jquery-append + $('#activate-account-modal p').append( + // xss-lint: disable=javascript-concat-html + '<svg class="fa-pulse" style="vertical-align:bottom" width="24" height="24"' + + // xss-lint: disable=javascript-concat-html + 'viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">\n' + + // xss-lint: disable=javascript-concat-html + '<path d="M22 12A10 10 0 116.122 3.91l1.176 1.618A8 8 0 1020 12h2z" fill="#6c757d"/>\n' + + '</svg>' + ); + }); + + $('#activate-account-modal').on('click', '#button', function() { + $('#activate-account-modal').css('display', 'none'); + $('#lean_overlay').css({display: 'none'}); + }); + if ($('#activate-account-modal').css('display') === 'block') { + $('#lean_overlay').css({ + display: 'block', + 'z-index': 0 + }); + } + $('.action-email-settings').each(function(index) { $(this).attr('id', 'email-settings-' + index); // a bit of a hack, but gets the unique selector for the modal trigger diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss index c5ab1c49b36b8826e45017a81400d5140611acc0..b12bcd0fca95e396512e74a616158058718e03f6 100644 --- a/lms/static/sass/multicourse/_dashboard.scss +++ b/lms/static/sass/multicourse/_dashboard.scss @@ -1641,6 +1641,59 @@ a.fade-cover { } } +#activate-account-modal { + display: block; + position: fixed; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + margin-top: -60px; + text-align: left; + height: 252px; + width: 450px; + box-shadow: none; + background: none; + + .inner-wrapper { + h3 { + font-family: inherit; + padding: 1.5rem 1rem 1rem 1rem; + text-align: left; + font-weight: bold; + font-size: 1.3rem; + line-height: 1.75rem; + } + border: none; + border-radius: 5px; + } +} + +@media (max-width: 460px) and (min-width: 360px) { + #activate-account-modal { + width: 350px; + } +} + +@media (max-width: 360px) { + #activate-account-modal { + width: 300px; + } +} + +.activate-account-modal-button { + text-align: right; + display: block; + padding: 1rem 1rem 1rem 0; + + .btn-primary { + text-transform: none; + font-weight: 500; + } +} +.activate-account-modal-body { + padding: 0 1rem; +} + .reasons_survey { margin: 20px; diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index b0351a41cbdc4eeaa07185561d100d8418e5bd60..f9943bc142d111e0b4678fd42e88fa213ab99c0d 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -46,7 +46,9 @@ from common.djangoapps.student.models import CourseEnrollment edx.dashboard.legacy.init({ dashboard: "${reverse('dashboard') | n, js_escaped_string}", signInUser: "${reverse('signin_user') | n, js_escaped_string}", - changeEmailSettings: "${reverse('change_email_settings') | n, js_escaped_string}" + changeEmailSettings: "${reverse('change_email_settings') | n, js_escaped_string}", + sendAccountActivationEmail: "${reverse('send_account_activation_email') | n, js_escaped_string}" + }); }); </script> @@ -316,6 +318,35 @@ from common.djangoapps.student.models import CourseEnrollment </div> </main> +%if show_account_activation_popup: + <div id="activate-account-modal" class="modal activate-account-modal" aria-hidden="true"> + <div class="inner-wrapper" role="dialog" aria-labelledby="activate-account-modal-title" aria-live="polite"> + <h3> + ${_("Activate your account so you can log back in")} + <span class="sr">, + ## Translators: this text gives status on if the modal interface (a menu or piece of UI that takes the full focus of the screen) is open or not + ${_("window open")} + </span> + </h3> + <p class="activate-account-modal-body">${Text(_("We sent an email to {strong_start}{email}{strong_end} with a link to activate your account. Can’t find it? Check your spam folder or {link_start}resend the email{link_end}.")).format( + strong_start=HTML('<strong>'), + email=user.email, + strong_end=HTML('</strong>'), + link_start=HTML('<a href="#" id="send_cta_email" >'), + link_end=HTML('</a>') + )} + </p> + <div class="activate-account-modal-button"> + <button class="btn btn-primary" id="button"> + ${Text(_("Continue to {platform_name}")).format(platform_name=settings.PLATFORM_NAME)} + <svg style="vertical-align:bottom" width="24" height="24" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></svg> + </button> + </div> + + </div> + </div> +%endif + <div id="email-settings-modal" class="modal" aria-hidden="true"> <div class="inner-wrapper" role="dialog" aria-labelledby="email-settings-title"> <button class="close-modal"> diff --git a/lms/templates/main.html b/lms/templates/main.html index 741a4092fd652ab7299e2698c45447f5c33d8f91..4e81c012317aff437fe9fe5e86660738cb8ef9ba 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -213,6 +213,7 @@ from common.djangoapps.pipeline_mako import render_require_js_path_overrides <script type="text/javascript" src="${static.url('js/header/header.js')}"></script> <%static:optional_include_mako file="body-extra.html" is_theming_enabled="True" /> <script type="text/javascript" src="${static.url('js/src/jquery_extend_patch.js')}"></script> + <div id="lean_overlay"></div> </body> </html> diff --git a/openedx/core/djangoapps/user_authn/api/tests/test_views.py b/openedx/core/djangoapps/user_authn/api/tests/test_views.py index 8edef4ee01a576de30e5e50d2568fdbc5524e4a8..efa16168f67173a457801f5563504d927535a8db 100644 --- a/openedx/core/djangoapps/user_authn/api/tests/test_views.py +++ b/openedx/core/djangoapps/user_authn/api/tests/test_views.py @@ -9,6 +9,9 @@ from django.conf import settings from django.urls import reverse from rest_framework.test import APITestCase +from common.djangoapps.student.models import Registration +from common.djangoapps.student.tests.factories import UserFactory +from openedx.core.djangoapps.user_api.tests.test_views import UserAPITestCase from openedx.core.djangolib.testing.utils import skip_unless_lms from common.djangoapps.third_party_auth import pipeline from common.djangoapps.third_party_auth.tests.testutil import ThirdPartyAuthTestMixin, simulate_running_pipeline @@ -175,3 +178,32 @@ class MFEContextViewTest(ThirdPartyAuthTestMixin, APITestCase): assert response.status_code == 200 assert response.data['countryCode'] == self.country_code + + +@skip_unless_lms +class SendAccountActivationEmail(UserAPITestCase): + """ + Test for send activation email view + """ + + def setUp(self): + """ + Create a user, then log in. + """ + super().setUp() + self.user = UserFactory() + Registration().register(self.user) + result = self.client.login(username=self.user.username, password="test") + assert result, 'Could not log in' + self.path = reverse('send_account_activation_email') + + @patch('common.djangoapps.student.views.management.compose_activation_email') + def test_send_email_to_inactive_user_via_cta_dialog(self, email): + """ + Tests when user clicks on resend activation email on CTA dialog box, system + sends an activation email to the user. + """ + self.user.is_active = False + self.user.save() + self.client.post(self.path) + assert email.called is True, 'method should have been called' diff --git a/openedx/core/djangoapps/user_authn/api/urls.py b/openedx/core/djangoapps/user_authn/api/urls.py index 2e9d4993f07c1a6f7537857a2dac91a00ba4dcf0..51c84eb6834f3ba7cd8c7ca7dbf3bda6bf1b78d8 100644 --- a/openedx/core/djangoapps/user_authn/api/urls.py +++ b/openedx/core/djangoapps/user_authn/api/urls.py @@ -4,9 +4,14 @@ Authn API urls from django.conf.urls import url -from openedx.core.djangoapps.user_authn.api.views import MFEContextView +from openedx.core.djangoapps.user_authn.api.views import MFEContextView, SendAccountActivationEmail urlpatterns = [ url(r'^third_party_auth_context$', MFEContextView.as_view(), name='third_party_auth_context'), url(r'^mfe_context$', MFEContextView.as_view(), name='mfe_context'), + url( + r'^send_account_activation_email$', + SendAccountActivationEmail.as_view(), + name='send_account_activation_email' + ), ] diff --git a/openedx/core/djangoapps/user_authn/api/views.py b/openedx/core/djangoapps/user_authn/api/views.py index 08a631e843d05bcd46e8044f58eadeeb8980714c..b1789cb6d4a3a5976048bbce9b89604439cd62d7 100644 --- a/openedx/core/djangoapps/user_authn/api/views.py +++ b/openedx/core/djangoapps/user_authn/api/views.py @@ -7,9 +7,12 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.throttling import AnonRateThrottle from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser from common.djangoapps.student.helpers import get_next_url_for_login_page from openedx.core.djangoapps.user_authn.views.utils import get_mfe_context +from common.djangoapps.student.views import compose_and_send_activation_email class MFEContextThrottle(AnonRateThrottle): @@ -45,3 +48,29 @@ class MFEContextView(APIView): status=status.HTTP_200_OK, data=context ) + + +class SendAccountActivationEmail(APIView): + """ + API to to send the account activation email using account activation cta. + """ + authentication_classes = (SessionAuthenticationAllowInactiveUser,) + permission_classes = (IsAuthenticated,) + + def post(self, request, **kwargs): # lint-amnesty, pylint: disable=unused-argument + """ + Returns status code. + Arguments: + request (HttpRequest): The request, used to get the user + """ + try: + user = request.user + if not user.is_active: + compose_and_send_activation_email(user, user.profile) + return Response( + status=status.HTTP_200_OK + ) + except Exception: # pylint: disable=broad-except + return Response( + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) diff --git a/openedx/core/djangoapps/user_authn/views/register.py b/openedx/core/djangoapps/user_authn/views/register.py index b1afc171261a9ebcab1343fa129f83c29df96a04..6c253e3fdf922a13c623d515ac7b8aee4e6d4b63 100644 --- a/openedx/core/djangoapps/user_authn/views/register.py +++ b/openedx/core/djangoapps/user_authn/views/register.py @@ -538,6 +538,14 @@ class RegistrationView(APIView): redirect_url = get_redirect_url_with_host(root_url, redirect_to) response = self._create_response(request, {}, status_code=200, redirect_url=redirect_url) set_logged_in_cookies(request, response, user) + if not user.is_active and settings.SHOW_ACCOUNT_ACTIVATION_CTA: + response.set_cookie( + settings.SHOW_ACTIVATE_CTA_POPUP_COOKIE_NAME, + True, + domain=settings.SESSION_COOKIE_DOMAIN, + path='/', + secure=request.is_secure() + ) # setting the cookie to show account activation dialogue in platform and learning MFE return response def _handle_duplicate_email_username(self, request, data):