MST-542 Roll out Accounts MFE IDV workflow (#25613)

* MST-542 remove the IDV redirect to Account MFE waffle flag to permanently redirect learners to new IDV workflow. This completes the rollout process on edx-platform
This commit is contained in:
Simon Chen
2020-12-01 09:08:12 -05:00
committed by GitHub
parent 0cf010b763
commit 00ad36839d
15 changed files with 62 additions and 137 deletions

View File

@@ -20,6 +20,7 @@ from common.djangoapps.course_modes.models import CourseMode, Mode
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
from lms.djangoapps.commerce.tests import test_utils as ecomm_test_utils
from lms.djangoapps.commerce.tests.mocks import mock_payment_processors
from lms.djangoapps.verify_student.services import IDVerificationService
from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin
from openedx.core.djangoapps.embargo.test_utils import restrict_course
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme
@@ -116,11 +117,10 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
# Configure whether we're upgrading or not
url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
response = self.client.get(url)
start_flow_url = IDVerificationService.get_verify_location(course_id=self.course.id)
# Check whether we were correctly redirected
purchase_workflow = "?purchase_workflow=single"
start_flow_url = reverse('verify_student_start_flow', args=[six.text_type(self.course.id)]) + purchase_workflow
with mock_payment_processors():
self.assertRedirects(response, start_flow_url)
self.assertRedirects(response, start_flow_url, fetch_redirect_response=False)
def test_no_id_redirect_otto(self):
# Create the course modes
@@ -266,10 +266,8 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
# Since the only available track is professional ed, expect that
# we're redirected immediately to the start of the payment flow.
purchase_workflow = "?purchase_workflow=single"
start_flow_url = reverse('verify_student_start_flow', args=[six.text_type(self.course.id)]) + purchase_workflow
with mock_payment_processors():
self.assertRedirects(response, start_flow_url)
start_flow_url = IDVerificationService.get_verify_location(course_id=self.course.id)
self.assertRedirects(response, start_flow_url, fetch_redirect_response=False)
# Now enroll in the course
CourseEnrollmentFactory(
@@ -312,15 +310,12 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
if expected_redirect == 'dashboard':
redirect_url = reverse('dashboard')
elif expected_redirect == 'start-flow':
redirect_url = reverse(
'verify_student_start_flow',
kwargs={'course_id': six.text_type(self.course.id)}
)
redirect_url = IDVerificationService.get_verify_location(course_id=self.course.id)
else:
self.fail("Must provide a valid redirect URL name")
with mock_payment_processors(expect_called=None):
self.assertRedirects(response, redirect_url)
self.assertRedirects(response, redirect_url, fetch_redirect_response=False,)
def test_choose_mode_audit_enroll_on_post(self):
audit_mode = 'audit'

View File

@@ -112,8 +112,7 @@ class ChooseModeView(View):
has_enrolled_professional = (CourseMode.is_professional_slug(enrollment_mode) and is_active)
if CourseMode.has_professional_mode(modes) and not has_enrolled_professional:
purchase_workflow = request.GET.get("purchase_workflow", "single")
verify_url = IDVerificationService.get_verify_location('verify_student_start_flow', course_id=course_key)
redirect_url = "{url}?purchase_workflow={workflow}".format(url=verify_url, workflow=purchase_workflow)
redirect_url = IDVerificationService.get_verify_location(course_id=course_key)
if ecommerce_service.is_enabled(request.user):
professional_mode = modes.get(CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get(CourseMode.PROFESSIONAL)
if purchase_workflow == "single" and professional_mode.sku:
@@ -311,7 +310,7 @@ class ChooseModeView(View):
donation_for_course[six.text_type(course_key)] = amount_value
request.session["donation_for_course"] = donation_for_course
verify_url = IDVerificationService.get_verify_location('verify_student_start_flow', course_id=course_key)
verify_url = IDVerificationService.get_verify_location(course_id=course_key)
return redirect(verify_url)
def _get_requested_mode(self, request_dict):

View File

@@ -127,11 +127,9 @@ class ProgressTabView(RetrieveAPIView):
verification_status = IDVerificationService.user_status(request.user)
verification_link = None
if verification_status['status'] is None or verification_status['status'] == 'expired':
verification_link = IDVerificationService.get_verify_location('verify_student_verify_now',
course_id=course_key)
verification_link = IDVerificationService.get_verify_location(course_id=course_key)
elif verification_status['status'] == 'must_reverify':
verification_link = IDVerificationService.get_verify_location('verify_student_reverify',
course_id=course_key)
verification_link = IDVerificationService.get_verify_location(course_id=course_key)
verification_data = {
'link': verification_link,
'status': verification_status['status'],

View File

@@ -664,11 +664,11 @@ class VerificationDeadlineDate(DateSummary):
'verification-deadline-passed': (_('Learn More'), ''),
'verification-deadline-retry': (
_('Retry Verification'),
IDVerificationService.get_verify_location('verify_student_reverify'),
IDVerificationService.get_verify_location(),
),
'verification-deadline-upcoming': (
_('Verify My Identity'),
IDVerificationService.get_verify_location('verify_student_verify_now', self.course_id),
IDVerificationService.get_verify_location(self.course_id),
)
}

View File

@@ -37,6 +37,7 @@ from lms.djangoapps.courseware.models import (
)
from lms.djangoapps.experiments.testutils import override_experiment_waffle_flag
from lms.djangoapps.verify_student.models import VerificationDeadline
from lms.djangoapps.verify_student.services import IDVerificationService
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.schedules.signals import CREATE_SCHEDULE_WAFFLE_FLAG
@@ -679,7 +680,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
'You must successfully complete verification before this date to qualify for a Verified Certificate.'
)
self.assertEqual(block.link_text, 'Verify My Identity')
self.assertEqual(block.link, reverse('verify_student_verify_now', args=(course.id,)))
self.assertEqual(block.link, IDVerificationService.get_verify_location(course.id))
def test_verification_deadline_date_retry(self):
with freeze_time('2015-01-02'):
@@ -696,7 +697,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
'You must successfully complete verification before this date to qualify for a Verified Certificate.'
)
self.assertEqual(block.link_text, 'Retry Verification')
self.assertEqual(block.link, reverse('verify_student_reverify'))
self.assertEqual(block.link, IDVerificationService.get_verify_location())
def test_verification_deadline_date_denied(self):
with freeze_time('2015-01-02'):

View File

@@ -17,7 +17,6 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_
from common.djangoapps.student.models import User
from .models import ManualVerification, SoftwareSecurePhotoVerification, SSOVerification
from .toggles import redirect_to_idv_microfrontend
from .utils import earliest_allowed_verification_date, most_recent_verification, active_verifications
log = logging.getLogger(__name__)
@@ -49,7 +48,7 @@ class XBlockVerificationService(object):
"""
Returns the URL for a user to verify themselves.
"""
return IDVerificationService.get_verify_location('verify_student_reverify')
return IDVerificationService.get_verify_location()
class IDVerificationService(object):
@@ -232,24 +231,12 @@ class IDVerificationService(object):
return 'ID Verified'
@classmethod
def get_verify_location(cls, url_name, course_id=None):
def get_verify_location(cls, course_id=None):
"""
url_name is one of:
'verify_student_verify_now'
'verify_student_reverify'
Returns a string:
If waffle flag is active, returns URL for IDV microfrontend.
Else, returns URL for corresponding view.
Returns URL for IDV on Account Microfrontend
"""
location = ''
if redirect_to_idv_microfrontend():
location = '{}/id-verification'.format(settings.ACCOUNT_MICROFRONTEND_URL)
if course_id:
location = location + '?{}'.format(str(course_id))
else:
if course_id:
location = reverse(url_name, args=[str(course_id)])
else:
location = reverse(url_name)
location = '{}/id-verification'.format(settings.ACCOUNT_MICROFRONTEND_URL)
if course_id:
location = location + '?{}'.format(str(course_id))
return location

View File

@@ -6,12 +6,13 @@ import six
from django.urls import reverse
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
from lms.djangoapps.commerce.tests.mocks import mock_payment_processors
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from ..services import IDVerificationService
class TestProfEdVerification(ModuleStoreTestCase):
"""
@@ -34,31 +35,26 @@ class TestProfEdVerification(ModuleStoreTestCase):
min_price=self.MIN_PRICE,
suggested_prices=''
)
purchase_workflow = "?purchase_workflow=single"
self.urls = {
'course_modes_choose': reverse(
'course_modes_choose',
args=[six.text_type(self.course_key)]
),
'verify_student_start_flow': reverse(
'verify_student_start_flow',
args=[six.text_type(self.course_key)]
) + purchase_workflow,
'verify_student_start_flow': IDVerificationService.get_verify_location(self.course_key),
}
def test_start_flow(self):
# Go to the course mode page, expecting a redirect to the intro step of the
# payment flow (since this is a professional ed course). Otherwise, the student
# would have the option to choose their track.
with mock_payment_processors():
resp = self.client.get(self.urls['course_modes_choose'], follow=True)
self.assertRedirects(resp, self.urls['verify_student_start_flow'])
resp = self.client.get(self.urls['course_modes_choose'])
self.assertRedirects(
resp,
self.urls['verify_student_start_flow'],
fetch_redirect_response=False,
)
# For professional ed courses, expect that the student is NOT enrolled
# automatically in the course.
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_key))
# On the first page of the flow, verify that there's a button allowing the user
# to proceed to the payment processor; this is the only action the user is allowed to take.
self.assertContains(resp, 'payment-button')

View File

@@ -32,6 +32,7 @@ from lms.djangoapps.commerce.tests import TEST_API_URL, TEST_PAYMENT_DATA, TEST_
from lms.djangoapps.commerce.tests.mocks import mock_payment_processors
from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline
from lms.djangoapps.verify_student.services import IDVerificationService
from lms.djangoapps.verify_student.views import PayAndVerifyView, checkout_with_ecommerce_service, render_to_response
from openedx.core.djangoapps.embargo.test_utils import restrict_course
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme
@@ -959,9 +960,8 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin, Tes
def _assert_redirects_to_verify_start(self, response, course_id, status_code=302):
"""Check that the page redirects to the "verify later" part of the flow. """
url = reverse('verify_student_verify_now', kwargs={'course_id': six.text_type(course_id)})
with mock_payment_processors():
self.assertRedirects(response, url, status_code)
url = IDVerificationService.get_verify_location(course_id=course_id)
self.assertRedirects(response, url, status_code, fetch_redirect_response=False)
def _assert_redirects_to_upgrade(self, response, course_id):
"""Check that the page redirects to the "upgrade" part of the flow. """
@@ -1713,13 +1713,13 @@ class TestReverifyView(TestVerificationBase):
"""
Test that a User can use re-verify link for initial verification.
"""
self._assert_can_reverify()
self._assert_reverify()
def test_reverify_view_can_reverify_denied(self):
# User has a denied attempt, so can re-verify
attempt = self.create_and_submit_attempt_for_user(self.user)
attempt.deny("error")
self._assert_can_reverify()
self._assert_reverify()
def test_reverify_view_can_reverify_expired(self):
# User has a verification attempt, but it's expired
@@ -1731,7 +1731,7 @@ class TestReverifyView(TestVerificationBase):
attempt.save()
# Allow the student to re-verify
self._assert_can_reverify()
self._assert_reverify()
def test_reverify_view_can_reverify_pending(self):
""" Test that the user can still re-verify even if the previous photo
@@ -1746,7 +1746,7 @@ class TestReverifyView(TestVerificationBase):
self.create_and_submit_attempt_for_user(self.user)
# Can re-verify because an attempt has already been submitted.
self._assert_can_reverify()
self._assert_reverify()
def test_reverify_view_cannot_reverify_approved(self):
# Submitted attempt has been approved
@@ -1754,7 +1754,7 @@ class TestReverifyView(TestVerificationBase):
attempt.approve()
# Cannot re-verify because the user is already verified.
self._assert_cannot_reverify()
self._assert_reverify()
@override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10})
def test_reverify_view_can_reverify_approved_expired_soon(self):
@@ -1768,25 +1768,10 @@ class TestReverifyView(TestVerificationBase):
attempt.approve()
# Can re-verify because verification is set to expired soon.
self._assert_can_reverify()
self._assert_reverify()
def _get_reverify_page(self):
"""
Retrieve the re-verification page and return the response.
"""
def _assert_reverify(self):
url = reverse("verify_student_reverify")
return self.client.get(url)
def _assert_can_reverify(self):
"""
Check that the re-verification flow is rendered.
"""
response = self._get_reverify_page()
self.assertContains(response, "reverify-container")
def _assert_cannot_reverify(self):
"""
Check that the user is blocked from re-verifying.
"""
response = self._get_reverify_page()
self.assertContains(response, "reverify-blocked")
response = self.client.get(url)
verification_start_url = IDVerificationService.get_verify_location()
self.assertRedirects(response, verification_start_url, fetch_redirect_response=False)

View File

@@ -27,24 +27,3 @@ USE_NEW_EMAIL_TEMPLATES = WaffleFlag(
def use_new_templates_for_id_verification_emails():
return USE_NEW_EMAIL_TEMPLATES.is_enabled()
# Waffle flag to redirect to the new IDV flow on the account microfrontend
# .. toggle_name: verify_student.redirect_to_idv_microfrontend
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
# .. toggle_description: Supports staged rollout to students for the new IDV flow.
# .. toggle_use_cases: temporary, open_edx
# .. toggle_creation_date: 2020-07-09
# .. toggle_target_removal_date: None
# .. toggle_warnings: This temporary feature toggle does not have a target removal date.
# .. toggle_tickets: MST-318
REDIRECT_TO_IDV_MICROFRONTEND = WaffleFlag(
waffle_namespace=WAFFLE_FLAG_NAMESPACE,
flag_name='redirect_to_idv_microfrontend',
module_name=__name__,
)
def redirect_to_idv_microfrontend():
return REDIRECT_TO_IDV_MICROFRONTEND.is_enabled()

View File

@@ -52,7 +52,6 @@ from common.djangoapps.util.json_request import JsonResponse
from xmodule.modulestore.django import modulestore
from .services import IDVerificationService
from .toggles import redirect_to_idv_microfrontend
log = logging.getLogger(__name__)
@@ -499,10 +498,7 @@ class PayAndVerifyView(View):
if is_enrolled:
if already_paid:
# If the student has paid, but not verified, redirect to the verification flow.
url = IDVerificationService.get_verify_location(
'verify_student_verify_now',
six.text_type(course_key)
)
url = IDVerificationService.get_verify_location(six.text_type(course_key))
else:
url = reverse('verify_student_start_flow', kwargs=course_kwargs)
@@ -1216,19 +1212,5 @@ class ReverifyView(View):
Most of the work is done client-side by composing the same
Backbone views used in the initial verification flow.
"""
verification_status = IDVerificationService.user_status(request.user)
expiration_datetime = IDVerificationService.get_expiration_datetime(request.user, ['approved'])
if can_verify_now(verification_status, expiration_datetime):
if redirect_to_idv_microfrontend():
return redirect('{}/id-verification'.format(settings.ACCOUNT_MICROFRONTEND_URL))
context = {
"user_full_name": request.user.profile.name,
"platform_name": configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
"capture_sound": staticfiles_storage.url("audio/camera_capture.wav"),
}
return render_to_response("verify_student/reverify.html", context)
else:
context = {
"status": verification_status['status']
}
return render_to_response("verify_student/reverify_not_allowed.html", context)
IDV_workflow = IDVerificationService.get_verify_location()
return redirect(IDV_workflow)

View File

@@ -570,7 +570,7 @@ PDF_RECEIPT_TAX_ID_LABEL = 'Tax ID'
PROFILE_MICROFRONTEND_URL = "http://profile-mfe/abc/"
ORDER_HISTORY_MICROFRONTEND_URL = "http://order-history-mfe/"
ACCOUNT_MICROFRONTEND_URL = "http://account-mfe/"
ACCOUNT_MICROFRONTEND_URL = "http://account-mfe"
LOGISTRATION_MICROFRONTEND_URL = "http://logistation-mfe"
LOGISTRATION_MICROFRONTEND_DOMAIN = "logistation-mfe"
LEARNING_MICROFRONTEND_URL = "http://learning-mfe"

View File

@@ -29,7 +29,7 @@ from lms.djangoapps.experiments.utils import UPSELL_TRACKING_FLAG
%>
<%
reverify_link = IDVerificationService.get_verify_location('verify_student_reverify')
reverify_link = IDVerificationService.get_verify_location()
cert_name_short = course_overview.cert_name_short
if cert_name_short == "":
cert_name_short = settings.CERT_NAME_SHORT
@@ -370,7 +370,7 @@ from lms.djangoapps.experiments.utils import UPSELL_TRACKING_FLAG
% endif
</div>
<div class="verification-cta">
<a href="${IDVerificationService.get_verify_location('verify_student_verify_now', course_overview.id)}" class="btn" data-course-id="${course_overview.id}">${_('Verify Now')}</a>
<a href="${IDVerificationService.get_verify_location(course_overview.id)}" class="btn" data-course-id="${course_overview.id}">${_('Verify Now')}</a>
</div>
% elif verification_status['status'] == VERIFY_STATUS_SUBMITTED:
<h4 class="message-title">${_('You have submitted your verification information.')}</h4>
@@ -388,7 +388,7 @@ from lms.djangoapps.experiments.utils import UPSELL_TRACKING_FLAG
## Translators: start_link and end_link will be replaced with HTML tags;
## please do not translate these.
<p class="message-copy">${Text(_('Your current verification will expire in {days} days. {start_link}Re-verify your identity now{end_link} using a webcam and a government-issued photo ID.')).format(
start_link=HTML('<a href="{href}">').format(href=IDVerificationService.get_verify_location('verify_student_reverify')),
start_link=HTML('<a href="{href}">').format(href=IDVerificationService.get_verify_location()),
end_link=HTML('</a>'),
days=settings.VERIFY_STUDENT.get("EXPIRING_SOON_WINDOW")
)}
@@ -420,7 +420,7 @@ from lms.djangoapps.experiments.utils import UPSELL_TRACKING_FLAG
% if use_ecommerce_payment_flow and course_mode_info['verified_sku']:
<a class="action action-upgrade track_course_dashboard_green_button" href="${ecommerce_payment_page}?sku=${course_mode_info['verified_sku']}">
% else:
<a class="action action-upgrade track_course_dashboard_green_button" href="${IDVerificationService.get_verify_location('verify_student_upgrade_and_verify', course_id=course_overview.id)}" data-course-id="${course_overview.id}" data-user="${user.username}">
<a class="action action-upgrade track_course_dashboard_green_button" href="${IDVerificationService.get_verify_location(course_id=course_overview.id)}" data-course-id="${course_overview.id}" data-user="${user.username}">
% endif
<span class="action-upgrade-icon" aria-hidden="true"></span>
<span class="wrapper-copy">

View File

@@ -14,7 +14,7 @@ from lms.djangoapps.verify_student.services import IDVerificationService
%if verification_expiry:
<p class="status-note"><span><b>${_("Warning")}: </b></span><i>${_("Your photo verification expires on {verification_expiry}. Please be aware photo verification can take up to three days once initiated and you will not be able to earn a certificate or take a proctored exam until approved.").format(verification_expiry=verification_expiry)}</i></p>
<div class="btn-reverify">
<a href="${IDVerificationService.get_verify_location('verify_student_reverify')}" class="action action-reverify">${_("Resubmit Verification")}</a>
<a href="${IDVerificationService.get_verify_location()}" class="action action-reverify">${_("Resubmit Verification")}</a>
</div>
%endif
</li>
@@ -40,7 +40,7 @@ from lms.djangoapps.verify_student.services import IDVerificationService
%endif
</p>
<div class="btn-reverify">
<a href="${IDVerificationService.get_verify_location('verify_student_reverify')}" class="action action-reverify">${_("Resubmit Verification")}</a>
<a href="${IDVerificationService.get_verify_location()}" class="action action-reverify">${_("Resubmit Verification")}</a>
</div>
</li>
%elif verification_status == 'expired':
@@ -49,7 +49,7 @@ from lms.djangoapps.verify_student.services import IDVerificationService
<p class="status-note">${_("Your verification has expired. To receive a verified certificate, you must submit a new photo of yourself and your government-issued photo ID before the verification deadline for your course.")}</p>
<p class="status-note"><span><b>${_("Warning")}: </b></span><i>${_(" Please be aware photo verification can take up to three days once initiated and you will not be able to earn a certificate or take a proctored exam until approved.")}</i></p>
<div class="btn-reverify">
<a href="${IDVerificationService.get_verify_location('verify_student_reverify')}" class="action action-reverify">${_("Resubmit Verification")}</a>
<a href="${IDVerificationService.get_verify_location()}" class="action action-reverify">${_("Resubmit Verification")}</a>
</div>
</li>
%endif

View File

@@ -19,6 +19,7 @@ from lms.djangoapps.certificates.tests.factories import (
from lms.djangoapps.courseware.access_utils import ACCESS_DENIED, ACCESS_GRANTED
from lms.djangoapps.courseware.tabs import ExternalLinkCourseTab
from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin
from lms.djangoapps.verify_student.services import IDVerificationService
from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentCelebration
from common.djangoapps.student.tests.factories import CourseEnrollmentCelebrationFactory, UserFactory
from xmodule.modulestore.django import modulestore
@@ -133,7 +134,9 @@ class CourseApiTestViews(BaseCoursewareTests):
assert response.data['linkedin_add_to_profile_url'] is None
else:
assert response.data['certificate_data']['cert_status'] == 'earned_but_not_available'
expected_verify_identity_url = reverse('verify_student_verify_now', args=[self.course.id])
expected_verify_identity_url = IDVerificationService.get_verify_location(
course_id=self.course.id
)
# The response contains an absolute URL so this is only checking the path of the final
assert expected_verify_identity_url in response.data['verify_identity_url']

View File

@@ -253,9 +253,9 @@ class CoursewareMeta:
if self.enrollment_object and self.enrollment_object.mode in CourseMode.VERIFIED_MODES:
verification_status = IDVerificationService.user_status(self.effective_user)['status']
if verification_status == 'must_reverify':
return IDVerificationService.get_verify_location('verify_student_reverify')
return IDVerificationService.get_verify_location()
else:
return IDVerificationService.get_verify_location('verify_student_verify_now', self.course_key)
return IDVerificationService.get_verify_location(self.course_key)
@property
def linkedin_add_to_profile_url(self):