From 80589eab3644ebc8b9c6c8dde9b7e668a24edf5e Mon Sep 17 00:00:00 2001 From: Renzo Lucioni Date: Wed, 21 Jan 2015 15:22:32 -0500 Subject: [PATCH] Remove old payment and verification flow Removes old payment and verification endpoints, views, templates, and tests, making the new split flow the default. The SEPARATE_VERIFICATION_FROM_PAYMENT feature flag is also removed. --- .../course_modes/tests/test_views.py | 28 +- common/djangoapps/course_modes/views.py | 56 +- .../student/tests/test_verification_status.py | 6 +- common/djangoapps/student/tests/tests.py | 35 +- common/djangoapps/student/views.py | 21 +- common/test/acceptance/pages/lms/dashboard.py | 21 +- .../acceptance/pages/lms/pay_and_verify.py | 8 +- .../acceptance/pages/lms/track_selection.py | 19 +- common/test/acceptance/tests/lms/test_lms.py | 16 +- .../courseware/features/certificates.feature | 91 - .../courseware/features/certificates.py | 278 --- lms/djangoapps/shoppingcart/models.py | 36 +- .../shoppingcart/tests/test_models.py | 24 +- .../shoppingcart/tests/test_views.py | 38 +- lms/djangoapps/shoppingcart/views.py | 34 +- .../verify_student/tests/test_integration.py | 80 +- .../verify_student/tests/test_views.py | 1672 ++++++++--------- lms/djangoapps/verify_student/urls.py | 163 +- lms/djangoapps/verify_student/views.py | 155 -- lms/envs/bok_choy.env.json | 1 - lms/envs/common.py | 3 - .../dashboard/_dashboard_course_listing.html | 104 +- .../emails/order_confirmation_email.txt | 2 +- .../verify_student/final_verification.html | 10 - .../verify_student/photo_verification.html | 461 ----- .../verify_student/show_requirements.html | 187 -- lms/templates/verify_student/verified.html | 114 -- 27 files changed, 994 insertions(+), 2669 deletions(-) delete mode 100644 lms/djangoapps/courseware/features/certificates.feature delete mode 100644 lms/djangoapps/courseware/features/certificates.py delete mode 100644 lms/templates/verify_student/final_verification.html delete mode 100644 lms/templates/verify_student/photo_verification.html delete mode 100644 lms/templates/verify_student/show_requirements.html delete mode 100644 lms/templates/verify_student/verified.html diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index 32fbe8c939..bce5d52434 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -70,18 +70,6 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): else: self.assertEquals(response.status_code, 200) - def test_upgrade_copy(self): - # Create the course modes - for mode in ('audit', 'honor', 'verified'): - CourseModeFactory(mode_slug=mode, course_id=self.course.id) - - url = reverse('course_modes_choose', args=[unicode(self.course.id)]) - response = self.client.get(url, {"upgrade": True}) - - # Verify that the upgrade copy is displayed instead - # of the usual text. - self.assertContains(response, "Upgrade Your Enrollment") - def test_no_enrollment(self): # Create the course modes for mode in ('audit', 'honor', 'verified'): @@ -137,10 +125,10 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)]) response = self.client.get(choose_track_url) - # Expect that we're redirected immediately to the "show requirements" page - # (since the only available track is professional ed) - show_reqs_url = reverse('verify_student_show_requirements', args=[unicode(self.course.id)]) - self.assertRedirects(response, show_reqs_url) + # Since the only available track is professional ed, expect that + # we're redirected immediately to the start of the payment flow. + start_flow_url = reverse('verify_student_start_flow', args=[unicode(self.course.id)]) + self.assertRedirects(response, start_flow_url) # Now enroll in the course CourseEnrollmentFactory( @@ -164,7 +152,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): @ddt.data( ('honor', 'dashboard'), - ('verified', 'show_requirements'), + ('verified', 'start-flow'), ) @ddt.unpack def test_choose_mode_redirect(self, course_mode, expected_redirect): @@ -179,11 +167,11 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): # Verify the redirect if expected_redirect == 'dashboard': redirect_url = reverse('dashboard') - elif expected_redirect == 'show_requirements': + elif expected_redirect == 'start-flow': redirect_url = reverse( - 'verify_student_show_requirements', + 'verify_student_start_flow', kwargs={'course_id': unicode(self.course.id)} - ) + "?upgrade=False" + ) else: self.fail("Must provide a valid redirect URL name") diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index c3a393e212..7d022fed31 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -52,19 +52,6 @@ class ChooseModeView(View): """ course_key = CourseKey.from_string(course_id) - upgrade = request.GET.get('upgrade', False) - request.session['attempting_upgrade'] = upgrade - - # TODO (ECOM-188): Once the A/B test of decoupled/verified flows - # completes, we can remove this flag. - # The A/B test framework will reload the page with the ?separate-verified GET param - # set if the user is in the experimental condition. We then store this flag - # in a session variable so downstream views can check it. - if request.GET.get('separate-verified', False): - request.session['separate-verified'] = True - elif request.GET.get('disable-separate-verified', False) and 'separate-verified' in request.session: - del request.session['separate-verified'] - enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(request.user, course_key) modes = CourseMode.modes_for_course_dict(course_key) @@ -73,22 +60,12 @@ class ChooseModeView(View): # to the usual "choose your track" page. has_enrolled_professional = (enrollment_mode == "professional" and is_active) if "professional" in modes and not has_enrolled_professional: - # TODO (ECOM-188): Once the A/B test of separating verification / payment completes, - # we can remove the check for the session variable. - if settings.FEATURES.get('SEPARATE_VERIFICATION_FROM_PAYMENT') and request.session.get('separate-verified', False): - return redirect( - reverse( - 'verify_student_start_flow', - kwargs={'course_id': unicode(course_key)} - ) - ) - else: - return redirect( - reverse( - 'verify_student_show_requirements', - kwargs={'course_id': unicode(course_key)} - ) + return redirect( + reverse( + 'verify_student_start_flow', + kwargs={'course_id': unicode(course_key)} ) + ) # If there isn't a verified mode available, then there's nothing # to do on this page. The user has almost certainly been auto-registered @@ -113,7 +90,6 @@ class ChooseModeView(View): "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, - "upgrade": upgrade, "can_audit": "audit" in modes, "responsive": True } @@ -156,8 +132,6 @@ class ChooseModeView(View): error_msg = _("Enrollment is closed") return self.get(request, course_id, error=error_msg) - upgrade = request.GET.get('upgrade', False) - requested_mode = self._get_requested_mode(request.POST) allowed_modes = CourseMode.modes_for_course_dict(course_key) @@ -192,22 +166,12 @@ class ChooseModeView(View): donation_for_course[unicode(course_key)] = amount_value request.session["donation_for_course"] = donation_for_course - # TODO (ECOM-188): Once the A/B test of separate verification flow completes, - # we can remove the check for the session variable. - if settings.FEATURES.get('SEPARATE_VERIFICATION_FROM_PAYMENT') and request.session.get('separate-verified', False): - return redirect( - reverse( - 'verify_student_start_flow', - kwargs={'course_id': unicode(course_key)} - ) - ) - else: - return redirect( - reverse( - 'verify_student_show_requirements', - kwargs={'course_id': unicode(course_key)} - ) + "?upgrade={}".format(upgrade) + return redirect( + reverse( + 'verify_student_start_flow', + kwargs={'course_id': unicode(course_key)} ) + ) def _get_requested_mode(self, request_dict): """Get the user's requested mode diff --git a/common/djangoapps/student/tests/test_verification_status.py b/common/djangoapps/student/tests/test_verification_status.py index 39fd885086..ae5575863c 100644 --- a/common/djangoapps/student/tests/test_verification_status.py +++ b/common/djangoapps/student/tests/test_verification_status.py @@ -28,10 +28,7 @@ MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, incl @override_settings(MODULESTORE=MODULESTORE_CONFIG) -@patch.dict(settings.FEATURES, { - 'SEPARATE_VERIFICATION_FROM_PAYMENT': True, - 'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True -}) +@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @ddt.ddt class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): @@ -40,7 +37,6 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): PAST = datetime.now(UTC) - timedelta(days=5) FUTURE = datetime.now(UTC) + timedelta(days=5) - @patch.dict(settings.FEATURES, {'SEPARATE_VERIFICATION_FROM_PAYMENT': True}) def setUp(self): # Invoke UrlResetMixin super(TestCourseVerificationStatus, self).setUp('verify_student.urls') diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index a747c37598..2a45f2d838 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -35,6 +35,7 @@ from xmodule.modulestore.tests.django_utils import TEST_DATA_MOCK_MODULESTORE from bulk_email.models import Optout # pylint: disable=import-error from certificates.models import CertificateStatuses # pylint: disable=import-error from certificates.tests.factories import GeneratedCertificateFactory # pylint: disable=import-error +from verify_student.models import SoftwareSecurePhotoVerification import shoppingcart # pylint: disable=import-error @@ -192,11 +193,20 @@ class DashboardTest(ModuleStoreTestCase): self.client = Client() @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') - def check_verification_status_on(self, mode, value): + def _check_verification_status_on(self, mode, value): """ Check that the css class and the status message are in the dashboard html. """ + CourseModeFactory(mode_slug=mode, course_id=self.course.id) CourseEnrollment.enroll(self.user, self.course.location.course_key, mode=mode) + + if mode == 'verified': + # Simulate a successful verification attempt + attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) + attempt.mark_ready() + attempt.submit() + attempt.approve() + response = self.client.get(reverse('dashboard')) self.assertContains(response, "class=\"course {0}\"".format(mode)) self.assertContains(response, value) @@ -207,16 +217,25 @@ class DashboardTest(ModuleStoreTestCase): Test that the certificate verification status for courses is visible on the dashboard. """ self.client.login(username="jack", password="test") - self.check_verification_status_on('verified', 'You\'re enrolled as a verified student') - self.check_verification_status_on('honor', 'You\'re enrolled as an honor code student') - self.check_verification_status_on('audit', 'You\'re auditing this course') + self._check_verification_status_on('verified', 'You\'re enrolled as a verified student') + self._check_verification_status_on('honor', 'You\'re enrolled as an honor code student') + self._check_verification_status_on('audit', 'You\'re auditing this course') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') - def check_verification_status_off(self, mode, value): + def _check_verification_status_off(self, mode, value): """ Check that the css class and the status message are not in the dashboard html. """ + CourseModeFactory(mode_slug=mode, course_id=self.course.id) CourseEnrollment.enroll(self.user, self.course.location.course_key, mode=mode) + + if mode == 'verified': + # Simulate a successful verification attempt + attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) + attempt.mark_ready() + attempt.submit() + attempt.approve() + response = self.client.get(reverse('dashboard')) self.assertNotContains(response, "class=\"course {0}\"".format(mode)) self.assertNotContains(response, value) @@ -228,9 +247,9 @@ class DashboardTest(ModuleStoreTestCase): if the verified certificates setting is off. """ self.client.login(username="jack", password="test") - self.check_verification_status_off('verified', 'You\'re enrolled as a verified student') - self.check_verification_status_off('honor', 'You\'re enrolled as an honor code student') - self.check_verification_status_off('audit', 'You\'re auditing this course') + self._check_verification_status_off('verified', 'You\'re enrolled as a verified student') + self._check_verification_status_off('honor', 'You\'re enrolled as an honor code student') + self._check_verification_status_off('audit', 'You\'re auditing this course') def test_course_mode_info(self): verified_mode = CourseModeFactory.create( diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 9cebd02dcc..0e2e4be5c6 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -573,22 +573,11 @@ def dashboard(request): # # If a course is not included in this dictionary, # there is no verification messaging to display. - # - # TODO (ECOM-188): After the A/B test completes, we can remove the check - # for the GET param and the session var. - # The A/B test framework will set the GET param for users in the experimental - # group; we then set the session var so downstream views can check this. - if settings.FEATURES.get("SEPARATE_VERIFICATION_FROM_PAYMENT") and request.GET.get('separate-verified', False): - request.session['separate-verified'] = True - verify_status_by_course = check_verify_status_by_course( - user, - course_enrollment_pairs, - all_course_modes - ) - else: - if request.GET.get('disable-separate-verified', False) and 'separate-verified' in request.session: - del request.session['separate-verified'] - verify_status_by_course = {} + verify_status_by_course = check_verify_status_by_course( + user, + course_enrollment_pairs, + all_course_modes + ) cert_statuses = { course.id: cert_info(request.user, course) diff --git a/common/test/acceptance/pages/lms/dashboard.py b/common/test/acceptance/pages/lms/dashboard.py index bb01c1e97a..4404a8f7ad 100644 --- a/common/test/acceptance/pages/lms/dashboard.py +++ b/common/test/acceptance/pages/lms/dashboard.py @@ -13,32 +13,15 @@ class DashboardPage(PageObject): Student dashboard, where the student can view courses she/he has registered for. """ - def __init__(self, browser, separate_verified=False): + def __init__(self, browser): """Initialize the page. Arguments: browser (Browser): The browser instance. - - Keyword Arguments: - separate_verified (Boolean): Whether to use the split payment and - verification flow. """ super(DashboardPage, self).__init__(browser) - if separate_verified: - self._querystring = "?separate-verified=1" - else: - self._querystring = "?disable-separate-verified=1" - - @property - def url(self): - """Return the URL corresponding to the dashboard.""" - url = "{base}/dashboard{querystring}".format( - base=BASE_URL, - querystring=self._querystring - ) - - return url + url = "{base}/dashboard".format(base=BASE_URL) def is_browser_on_page(self): return self.q(css='section.my-courses').present diff --git a/common/test/acceptance/pages/lms/pay_and_verify.py b/common/test/acceptance/pages/lms/pay_and_verify.py index c29d24a7ba..e0ed3d8557 100644 --- a/common/test/acceptance/pages/lms/pay_and_verify.py +++ b/common/test/acceptance/pages/lms/pay_and_verify.py @@ -12,11 +12,7 @@ from .dashboard import DashboardPage class PaymentAndVerificationFlow(PageObject): """Interact with the split payment and verification flow. - These pages are currently hidden behind the feature flag - `SEPARATE_VERIFICATION_FROM_PAYMENT`, which is enabled in - the Bok Choy settings. - - When enabled, the flow can be accessed at the following URLs: + The flow can be accessed at the following URLs: `/verify_student/start-flow/{course}/` `/verify_student/upgrade/{course}/` `/verify_student/verify-now/{course}/` @@ -121,7 +117,7 @@ class PaymentAndVerificationFlow(PageObject): else: raise Exception("The dashboard can only be accessed from the enrollment confirmation.") - DashboardPage(self.browser, separate_verified=True).wait_for_page() + DashboardPage(self.browser).wait_for_page() class FakePaymentPage(PageObject): diff --git a/common/test/acceptance/pages/lms/track_selection.py b/common/test/acceptance/pages/lms/track_selection.py index 6197edec33..86ce7488f1 100644 --- a/common/test/acceptance/pages/lms/track_selection.py +++ b/common/test/acceptance/pages/lms/track_selection.py @@ -14,33 +14,22 @@ class TrackSelectionPage(PageObject): This page can be accessed at `/course_modes/choose/{course_id}/`. """ - def __init__(self, browser, course_id, separate_verified=False): + def __init__(self, browser, course_id): """Initialize the page. Arguments: browser (Browser): The browser instance. course_id (unicode): The course in which the user is enrolling. - - Keyword Arguments: - separate_verified (Boolean): Whether to use the split payment and - verification flow when enrolling as verified. """ super(TrackSelectionPage, self).__init__(browser) self._course_id = course_id - self._separate_verified = separate_verified - - if self._separate_verified: - self._querystring = "?separate-verified=1" - else: - self._querystring = "?disable-separate-verified=1" @property def url(self): """Return the URL corresponding to the track selection page.""" - url = "{base}/course_modes/choose/{course_id}/{querystring}".format( + url = "{base}/course_modes/choose/{course_id}/".format( base=BASE_URL, - course_id=self._course_id, - querystring=self._querystring + course_id=self._course_id ) return url @@ -61,7 +50,7 @@ class TrackSelectionPage(PageObject): if mode == "honor": self.q(css="input[name='honor_mode']").click() - return DashboardPage(self.browser, separate_verified=self._separate_verified).wait_for_page() + return DashboardPage(self.browser).wait_for_page() elif mode == "verified": # Check the first contribution option, then click the enroll button self.q(css=".contribution-option > input").first.click() diff --git a/common/test/acceptance/tests/lms/test_lms.py b/common/test/acceptance/tests/lms/test_lms.py index f69efe83cd..74641054f9 100644 --- a/common/test/acceptance/tests/lms/test_lms.py +++ b/common/test/acceptance/tests/lms/test_lms.py @@ -253,12 +253,12 @@ class PayAndVerifyTest(UniqueCourseTest): """ super(PayAndVerifyTest, self).setUp() - self.track_selection_page = TrackSelectionPage(self.browser, self.course_id, separate_verified=True) + self.track_selection_page = TrackSelectionPage(self.browser, self.course_id) self.payment_and_verification_flow = PaymentAndVerificationFlow(self.browser, self.course_id) self.immediate_verification_page = PaymentAndVerificationFlow(self.browser, self.course_id, entry_point='verify-now') self.upgrade_page = PaymentAndVerificationFlow(self.browser, self.course_id, entry_point='upgrade') self.fake_payment_page = FakePaymentPage(self.browser, self.course_id) - self.dashboard_page = DashboardPage(self.browser, separate_verified=True) + self.dashboard_page = DashboardPage(self.browser) # Create a course CourseFixture( @@ -278,7 +278,7 @@ class PayAndVerifyTest(UniqueCourseTest): # Create a user and log them in AutoAuthPage(self.browser).visit() - # Navigate to the track selection page with the appropriate GET parameter in the URL + # Navigate to the track selection page self.track_selection_page.visit() # Enter the payment and verification flow by choosing to enroll as verified @@ -304,7 +304,7 @@ class PayAndVerifyTest(UniqueCourseTest): # Submit photos and proceed to the enrollment confirmation step self.payment_and_verification_flow.next_verification_step(self.immediate_verification_page) - # Navigate to the dashboard with the appropriate GET parameter in the URL + # Navigate to the dashboard self.dashboard_page.visit() # Expect that we're enrolled as verified in the course @@ -315,7 +315,7 @@ class PayAndVerifyTest(UniqueCourseTest): # Create a user and log them in AutoAuthPage(self.browser).visit() - # Navigate to the track selection page with the appropriate GET parameter in the URL + # Navigate to the track selection page self.track_selection_page.visit() # Enter the payment and verification flow by choosing to enroll as verified @@ -327,7 +327,7 @@ class PayAndVerifyTest(UniqueCourseTest): # Submit payment self.fake_payment_page.submit_payment() - # Navigate to the dashboard with the appropriate GET parameter in the URL + # Navigate to the dashboard self.dashboard_page.visit() # Expect that we're enrolled as verified in the course @@ -338,7 +338,7 @@ class PayAndVerifyTest(UniqueCourseTest): # Create a user, log them in, and enroll them in the honor mode AutoAuthPage(self.browser, course_id=self.course_id).visit() - # Navigate to the dashboard with the appropriate GET parameter in the URL + # Navigate to the dashboard self.dashboard_page.visit() # Expect that we're enrolled as honor in the course @@ -357,7 +357,7 @@ class PayAndVerifyTest(UniqueCourseTest): # Submit payment self.fake_payment_page.submit_payment() - # Navigate to the dashboard with the appropriate GET parameter in the URL + # Navigate to the dashboard self.dashboard_page.visit() # Expect that we're enrolled as verified in the course diff --git a/lms/djangoapps/courseware/features/certificates.feature b/lms/djangoapps/courseware/features/certificates.feature deleted file mode 100644 index 369905c1a6..0000000000 --- a/lms/djangoapps/courseware/features/certificates.feature +++ /dev/null @@ -1,91 +0,0 @@ -@shard_2 -Feature: LMS.Verified certificates - As a student, - In order to earn a verified certificate - I want to sign up for a verified certificate course. - - Scenario: I can audit a verified certificate course - Given I am logged in - When I select the audit track - Then I should see the course on my dashboard - And a "edx.course.enrollment.activated" server event is emitted - - Scenario: I can submit photos to verify my identity - Given I am logged in - When I select the verified track - And I go to step "1" - And I capture my "face" photo - And I approve my "face" photo - And I go to step "2" - And I capture my "photo_id" photo - And I approve my "photo_id" photo - And I go to step "3" - And I select a contribution amount - And I confirm that the details match - And I go to step "4" - Then I am at the payment page - - Scenario: I can pay for a verified certificate - Given I have submitted photos to verify my identity - When I submit valid payment information - Then I see that my payment was successful - - Scenario: Verified courses display correctly on dashboard - Given I have submitted photos to verify my identity - When I submit valid payment information - And I navigate to my dashboard - Then I see the course on my dashboard - And I see that I am on the verified track - And I do not see the upsell link on my dashboard - And a "edx.course.enrollment.activated" server event is emitted - - # Not easily automated -# Scenario: I can re-take photos -# Given I have submitted my "" photo -# When I retake my "" photo -# Then I see the new photo on the confirmation page. -# -# Examples: -# | PhotoType | -# | face | -# | ID | - -# # TODO: automate -# Scenario: I can edit identity information -# Given I have submitted face and ID photos -# When I edit my name -# Then I see the new name on the confirmation page. - - Scenario: I can return to the verify flow - Given I have submitted photos to verify my identity - When I leave the flow and return - Then I am at the verified page - - # TODO: automate -# Scenario: I can pay from the return flow -# Given I have submitted photos to verify my identity -# When I leave the flow and return -# And I press the payment button -# Then I am at the payment page - - Scenario: The upsell offer is on the dashboard if I am auditing - Given I am logged in - When I select the audit track - And I navigate to my dashboard - Then I see the upsell link on my dashboard - - Scenario: I can take the upsell offer and pay for it - Given I am logged in - And I select the audit track - And I navigate to my dashboard - When I see the upsell link on my dashboard - And I select the upsell link on my dashboard - And I select the verified track for upgrade - And I submit my photos and confirm - And I am at the payment page - And I submit valid payment information - And I navigate to my dashboard - Then I see the course on my dashboard - And I see that I am on the verified track - And a "edx.course.enrollment.activated" server event is emitted - And a "edx.course.enrollment.upgrade.succeeded" server event is emitted diff --git a/lms/djangoapps/courseware/features/certificates.py b/lms/djangoapps/courseware/features/certificates.py deleted file mode 100644 index 5c65225b14..0000000000 --- a/lms/djangoapps/courseware/features/certificates.py +++ /dev/null @@ -1,278 +0,0 @@ -# pylint: disable=missing-docstring -# pylint: disable=redefined-outer-name - -from lettuce import world, step -from lettuce.django import django_url -from nose.tools import assert_equal - - -def create_cert_course(): - world.clear_courses() - org = 'edx' - number = '999' - name = 'Certificates' - world.scenario_dict['COURSE'] = world.CourseFactory.create( - org=org, number=number, display_name=name) - world.scenario_dict['course_id'] = world.scenario_dict['COURSE'].id - world.UPSELL_LINK_CSS = u'.message-upsell a.action-upgrade[href*="{}"]'.format( - world.scenario_dict['course_id'] - ) - - honor_mode = world.CourseModeFactory.create( - course_id=world.scenario_dict['course_id'], - mode_slug='honor', - mode_display_name='honor mode', - min_price=0, - ) - - verfied_mode = world.CourseModeFactory.create( - course_id=world.scenario_dict['course_id'], - mode_slug='verified', - mode_display_name='verified cert course', - min_price=16, - suggested_prices='32,64,128', - currency='usd', - ) - - -def register(): - url = u'courses/{}/about'.format(world.scenario_dict['course_id']) - world.browser.visit(django_url(url)) - - world.css_click('section.intro a.register') - assert world.is_css_present('section.wrapper h3.title') - - -@step(u'I select the audit track$') -def select_the_audit_track(step): - create_cert_course() - register() - btn_css = 'input[name="honor_mode"]' - world.wait(1) # TODO remove this after troubleshooting JZ - world.css_find(btn_css) - world.css_click(btn_css) - - -def select_contribution(amount=32): - radio_css = 'input[value="{}"]'.format(amount) - world.css_click(radio_css) - assert world.css_find(radio_css).selected - - -def click_verified_track_button(): - world.wait_for_ajax_complete() - btn_css = 'input[value="Pursue a Verified Certificate"]' - world.css_click(btn_css) - - -@step(u'I select the verified track for upgrade') -def select_verified_track_upgrade(step): - select_contribution(32) - world.wait_for_ajax_complete() - btn_css = 'input[value="Upgrade Your Enrollment"]' - world.css_click(btn_css) - # TODO: might want to change this depending on the changes for upgrade - assert world.is_css_present('section.progress') - - -@step(u'I select the verified track$') -def select_the_verified_track(step): - create_cert_course() - register() - select_contribution(32) - click_verified_track_button() - assert world.is_css_present('section.progress') - - -@step(u'I should see the course on my dashboard$') -def should_see_the_course_on_my_dashboard(step): - course_css = 'li.course-item' - assert world.is_css_present(course_css) - - -@step(u'I go to step "([^"]*)"$') -def goto_next_step(step, step_num): - btn_css = { - '1': '#face_next_button', - '2': '#face_next_link', - '3': '#photo_id_next_link', - '4': '#pay_button', - } - next_css = { - '1': 'div#wrapper-facephoto.carousel-active', - '2': 'div#wrapper-idphoto.carousel-active', - '3': 'div#wrapper-review.carousel-active', - '4': 'div#wrapper-review.carousel-active', - } - world.css_click(btn_css[step_num]) - - # Pressing the button will advance the carousel to the next item - # and give the wrapper div the "carousel-active" class - assert world.css_find(next_css[step_num]) - - -@step(u'I capture my "([^"]*)" photo$') -def capture_my_photo(step, name): - - # Hard coded red dot image - image_data = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==' - snapshot_script = "$('#{}_image')[0].src = '{}';".format(name, image_data) - - # Mirror the javascript of the photo_verification.html page - world.browser.execute_script(snapshot_script) - world.browser.execute_script("$('#{}_capture_button').hide();".format(name)) - world.browser.execute_script("$('#{}_reset_button').show();".format(name)) - world.browser.execute_script("$('#{}_approve_button').show();".format(name)) - assert world.css_find('#{}_approve_button'.format(name)) - - -@step(u'I approve my "([^"]*)" photo$') -def approve_my_photo(step, name): - button_css = { - 'face': 'div#wrapper-facephoto li.control-approve', - 'photo_id': 'div#wrapper-idphoto li.control-approve', - } - wrapper_css = { - 'face': 'div#wrapper-facephoto', - 'photo_id': 'div#wrapper-idphoto', - } - - # Make sure that the carousel is in the right place - assert world.css_has_class(wrapper_css[name], 'carousel-active') - assert world.css_find(button_css[name]) - - # HACK: for now don't bother clicking the approve button for - # id_photo, because it is sending you back to Step 1. - # Come back and figure it out later. JZ Aug 29 2013 - if name == 'face': - world.css_click(button_css[name]) - - # Make sure you didn't advance the carousel - assert world.css_has_class(wrapper_css[name], 'carousel-active') - - -@step(u'I select a contribution amount$') -def select_contribution_amount(step): - select_contribution(32) - - -@step(u'I confirm that the details match$') -def confirm_details_match(step): - # First you need to scroll down on the page - # to make the element visible? - # Currently chrome is failing with ElementNotVisibleException - world.browser.execute_script("window.scrollTo(0,1024)") - - cb_css = 'input#confirm_pics_good' - world.css_click(cb_css) - assert world.css_find(cb_css).checked - - -@step(u'I am at the payment page') -def at_the_payment_page(step): - world.wait_for_present('input[name=transactionSignature]') - - -@step(u'I submit valid payment information$') -def submit_payment(step): - # First make sure that the page is done if it still executing - # an ajax query. - world.wait_for_ajax_complete() - button_css = 'input[value=Submit]' - world.css_click(button_css) - - -@step(u'I have submitted face and ID photos$') -def submitted_face_and_id_photos(step): - step.given('I am logged in') - step.given('I select the verified track') - step.given('I go to step "1"') - step.given('I capture my "face" photo') - step.given('I approve my "face" photo') - step.given('I go to step "2"') - step.given('I capture my "photo_id" photo') - step.given('I approve my "photo_id" photo') - step.given('I go to step "3"') - - -@step(u'I have submitted photos to verify my identity') -def submitted_photos_to_verify_my_identity(step): - step.given('I have submitted face and ID photos') - step.given('I select a contribution amount') - step.given('I confirm that the details match') - step.given('I go to step "4"') - - -@step(u'I submit my photos and confirm') -def submit_photos_and_confirm(step): - step.given('I go to step "1"') - step.given('I capture my "face" photo') - step.given('I approve my "face" photo') - step.given('I go to step "2"') - step.given('I capture my "photo_id" photo') - step.given('I approve my "photo_id" photo') - step.given('I go to step "3"') - step.given('I select a contribution amount') - step.given('I confirm that the details match') - step.given('I go to step "4"') - - -@step(u'I see that my payment was successful') -def see_that_my_payment_was_successful(step): - title = world.css_find('div.wrapper-content-main h3.title') - assert_equal(title.text, u'Congratulations! You are now verified on edX.') - - -@step(u'I navigate to my dashboard') -def navigate_to_my_dashboard(step): - world.css_click('span.avatar') - assert world.css_find('section.my-courses') - - -@step(u'I see the course on my dashboard') -def see_the_course_on_my_dashboard(step): - course_link_css = u'section.my-courses a[href*="{}"]'.format(world.scenario_dict['course_id']) - assert world.is_css_present(course_link_css) - - -@step(u'I see the upsell link on my dashboard') -def see_upsell_link_on_my_dashboard(step): - course_link_css = world.UPSELL_LINK_CSS - assert world.is_css_present(course_link_css) - - -@step(u'I do not see the upsell link on my dashboard') -def see_no_upsell_link(step): - course_link_css = world.UPSELL_LINK_CSS - assert world.is_css_not_present(course_link_css) - - -@step(u'I select the upsell link on my dashboard') -def select_upsell_link_on_my_dashboard(step): - # expand the upsell section - world.css_click('.message-upsell') - course_link_css = world.UPSELL_LINK_CSS - # click the actual link - world.css_click(course_link_css) - - -@step(u'I see that I am on the verified track') -def see_that_i_am_on_the_verified_track(step): - id_verified_css = 'li.course-item article.course.verified' - assert world.is_css_present(id_verified_css) - - -@step(u'I leave the flow and return$') -def leave_the_flow_and_return(step): - world.visit(u'verify_student/verified/{}/'.format(world.scenario_dict['course_id'])) - - -@step(u'I am at the verified page$') -def see_the_payment_page(step): - assert world.css_find('button#pay_button') - - -@step(u'I edit my name$') -def edit_my_name(step): - btn_css = 'a.retake-photos' - world.css_click(btn_css) diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index 4ce199aaff..32562455d1 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -1444,7 +1444,7 @@ class CertificateItem(OrderItem): "dashboard_url": reverse('dashboard'), } - def additional_instruction_text(self, **kwargs): + def additional_instruction_text(self): refund_reminder = _( "You have up to two weeks into the course to unenroll from the Verified Certificate option " "and receive a full refund. To receive your refund, contact {billing_email}. " @@ -1452,32 +1452,18 @@ class CertificateItem(OrderItem): "Please do NOT include your credit card information." ).format(billing_email=settings.PAYMENT_SUPPORT_EMAIL) - # TODO (ECOM-188): When running the A/B test for - # separating the verified / payment flow, we want to add some extra instructions - # for users in the experimental group. In order to know the user is in the experimental - # group, we need to check a session variable. But at this point in the code, - # we're so deep into the request handling stack that we don't have access to the request. - # The approach taken here is to have the email template check the request session - # and pass in a kwarg to this function if it's set. The template already has - # access to the request (via edxmako middleware), so we don't need to change - # too much to make this work. Once the A/B test completes, though, we should - # clean this up by removing the `**kwargs` param and skipping the check - # for the session variable. - if settings.FEATURES.get('SEPARATE_VERIFICATION_FROM_PAYMENT') and kwargs.get('separate_verification'): - domain = microsite.get_value('SITE_NAME', settings.SITE_NAME) - path = reverse('verify_student_verify_later', kwargs={'course_id': unicode(self.course_id)}) - verification_url = "http://{domain}{path}".format(domain=domain, path=path) + domain = microsite.get_value('SITE_NAME', settings.SITE_NAME) + path = reverse('verify_student_verify_later', kwargs={'course_id': unicode(self.course_id)}) + verification_url = "http://{domain}{path}".format(domain=domain, path=path) - verification_reminder = _( - "If you haven't verified your identity yet, please start the verification process ({verification_url})." - ).format(verification_url=verification_url) + verification_reminder = _( + "If you haven't verified your identity yet, please start the verification process ({verification_url})." + ).format(verification_url=verification_url) - return "{verification_reminder} {refund_reminder}".format( - verification_reminder=verification_reminder, - refund_reminder=refund_reminder - ) - else: - return refund_reminder + return "{verification_reminder} {refund_reminder}".format( + verification_reminder=verification_reminder, + refund_reminder=refund_reminder + ) @classmethod def verified_certificates_count(cls, course_id, status): diff --git a/lms/djangoapps/shoppingcart/tests/test_models.py b/lms/djangoapps/shoppingcart/tests/test_models.py index 394e4a4f4e..63d0703b76 100644 --- a/lms/djangoapps/shoppingcart/tests/test_models.py +++ b/lms/djangoapps/shoppingcart/tests/test_models.py @@ -40,7 +40,6 @@ from shoppingcart.exceptions import ( ) from opaque_keys.edx.locator import CourseLocator -from util.testing import UrlResetMixin # Since we don't need any XML course fixtures, use a modulestore configuration # that disables the XML modulestore. @@ -49,10 +48,9 @@ MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, incl @override_settings(MODULESTORE=MODULESTORE_CONFIG) @ddt.ddt -class OrderTest(UrlResetMixin, ModuleStoreTestCase): - @patch.dict(settings.FEATURES, {'SEPARATE_VERIFICATION_FROM_PAYMENT': True}) +class OrderTest(ModuleStoreTestCase): def setUp(self): - super(OrderTest, self).setUp('verify_student.urls') + super(OrderTest, self).setUp() self.user = UserFactory.create() course = CourseFactory.create() @@ -229,7 +227,6 @@ class OrderTest(UrlResetMixin, ModuleStoreTestCase): 'STORE_BILLING_INFO': True, } ) - @patch.dict(settings.FEATURES, {'SEPARATE_VERIFICATION_FROM_PAYMENT': False}) def test_purchase(self): # This test is for testing the subclassing functionality of OrderItem, but in # order to do this, we end up testing the specific functionality of @@ -237,21 +234,21 @@ class OrderTest(UrlResetMixin, ModuleStoreTestCase): cart = Order.get_cart_for_user(user=self.user) self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_key)) item = CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') - # course enrollment object should be created but still inactive + # Course enrollment object should be created but still inactive self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_key)) - # the analytics client pipes output to stderr when using the default client + # Analytics client pipes output to stderr when using the default client with patch('sys.stderr', sys.stdout.write): cart.purchase() self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_key)) - # test e-mail sending + # Test email sending self.assertEquals(len(mail.outbox), 1) self.assertEquals('Order Payment Confirmation', mail.outbox[0].subject) self.assertIn(settings.PAYMENT_SUPPORT_EMAIL, mail.outbox[0].body) self.assertIn(unicode(cart.total_cost), mail.outbox[0].body) self.assertIn(item.additional_instruction_text(), mail.outbox[0].body) - # Assert Google Analytics event fired for purchase. + # Verify Google Analytics event fired for purchase self.mock_tracker.track.assert_called_once_with( # pylint: disable=maybe-no-member self.user.id, 'Completed Order', @@ -273,15 +270,6 @@ class OrderTest(UrlResetMixin, ModuleStoreTestCase): context={'Google Analytics': {'clientId': None}} ) - def test_payment_separate_from_verification_email(self): - cart = Order.get_cart_for_user(user=self.user) - item = CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') - cart.purchase() - - self.assertEquals(len(mail.outbox), 1) - # Verify that the verification reminder appears in the sent email. - self.assertIn(item.additional_instruction_text(), mail.outbox[0].body) - def test_purchase_item_failure(self): # once again, we're testing against the specific implementation of # CertificateItem diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index 6f6b72f8c4..d5402724ee 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -30,7 +30,6 @@ from xmodule.modulestore.tests.django_utils import ( from xmodule.modulestore.tests.factories import CourseFactory from student.roles import CourseSalesAdminRole from util.date_utils import get_default_time_display -from util.testing import UrlResetMixin from shoppingcart.views import _can_download_report, _get_date_from_str from shoppingcart.models import ( @@ -1227,19 +1226,15 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): self._assert_404(reverse('shoppingcart.views.billing_details', args=[])) -# TODO (ECOM-188): Once we complete the A/B test of separate -# verified/payment flows, we can replace these tests -# with something more general. @override_settings(MODULESTORE=MODULESTORE_CONFIG) -class ReceiptRedirectTest(UrlResetMixin, ModuleStoreTestCase): +class ReceiptRedirectTest(ModuleStoreTestCase): """Test special-case redirect from the receipt page. """ COST = 40 PASSWORD = 'password' - @patch.dict(settings.FEATURES, {'SEPARATE_VERIFICATION_FROM_PAYMENT': True}) def setUp(self): - super(ReceiptRedirectTest, self).setUp('verify_student.urls') + super(ReceiptRedirectTest, self).setUp() self.user = UserFactory.create() self.user.set_password(self.PASSWORD) self.user.save() @@ -1259,7 +1254,6 @@ class ReceiptRedirectTest(UrlResetMixin, ModuleStoreTestCase): password=self.PASSWORD ) - @patch.dict(settings.FEATURES, {'SEPARATE_VERIFICATION_FROM_PAYMENT': True}) def test_show_receipt_redirect_to_verify_student(self): # Create other carts first # This ensures that the order ID and order item IDs do not match @@ -1277,12 +1271,6 @@ class ReceiptRedirectTest(UrlResetMixin, ModuleStoreTestCase): ) self.cart.purchase() - # Set the session flag indicating that the user is in the - # experimental group - session = self.client.session - session['separate-verified'] = True - session.save() - # Visit the receipt page url = reverse('shoppingcart.views.show_receipt', args=[self.cart.id]) resp = self.client.get(url) @@ -1299,28 +1287,6 @@ class ReceiptRedirectTest(UrlResetMixin, ModuleStoreTestCase): self.assertRedirects(resp, redirect_url) - @patch.dict(settings.FEATURES, {'SEPARATE_VERIFICATION_FROM_PAYMENT': True}) - def test_no_redirect_if_not_in_experimental_group(self): - # Purchase a verified certificate - CertificateItem.add_to_order( - self.cart, - self.course_key, - self.COST, - 'verified' - ) - self.cart.purchase() - - # We do NOT set the session flag indicating that the user is in - # the experimental group. - - # Visit the receipt page - url = reverse('shoppingcart.views.show_receipt', args=[self.cart.id]) - resp = self.client.get(url) - - # Since the user is not in the experimental group, expect - # that we see the usual receipt page (no redirect) - self.assertEqual(resp.status_code, 200) - @override_settings(MODULESTORE=MODULESTORE_CONFIG) @patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True}) diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py index eaa5eaefc2..8ed9666922 100644 --- a/lms/djangoapps/shoppingcart/views.py +++ b/lms/djangoapps/shoppingcart/views.py @@ -830,28 +830,28 @@ def _show_receipt_html(request, order): 'reg_code_info_list': reg_code_info_list, 'order_purchase_date': order.purchase_time.strftime("%B %d, %Y"), } - # we want to have the ability to override the default receipt page when - # there is only one item in the order + # We want to have the ability to override the default receipt page when + # there is only one item in the order. if order_items.count() == 1: receipt_template = order_items[0].single_item_receipt_template context.update(order_items[0].single_item_receipt_context) - # TODO (ECOM-188): Once the A/B test of separate verified / payment flow - # completes, implement this in a more general way. For now, - # we simply redirect to the new receipt page (in verify_student). - if settings.FEATURES.get('SEPARATE_VERIFICATION_FROM_PAYMENT') and request.session.get('separate-verified', False): - if receipt_template == 'shoppingcart/verified_cert_receipt.html': - url = reverse( - 'verify_student_payment_confirmation', - kwargs={'course_id': unicode(order_items[0].course_id)} - ) + # Ideally, the shoppingcart app would own the receipt view. However, + # as a result of changes made to the payment and verification flows as + # part of an A/B test, the verify_student app owns it instead. This is + # left over, and will be made more general in the future. + if receipt_template == 'shoppingcart/verified_cert_receipt.html': + url = reverse( + 'verify_student_payment_confirmation', + kwargs={'course_id': unicode(order_items[0].course_id)} + ) - # Add a query string param for the order ID - # This allows the view to query for the receipt information later. - url += '?payment-order-num={order_num}'.format( - order_num=order_items[0].order.id - ) - return HttpResponseRedirect(url) + # Add a query string param for the order ID + # This allows the view to query for the receipt information later. + url += '?payment-order-num={order_num}'.format( + order_num=order_items[0].order.id + ) + return HttpResponseRedirect(url) return render_to_response(receipt_template, context) diff --git a/lms/djangoapps/verify_student/tests/test_integration.py b/lms/djangoapps/verify_student/tests/test_integration.py index 72ac2aa919..99c7876907 100644 --- a/lms/djangoapps/verify_student/tests/test_integration.py +++ b/lms/djangoapps/verify_student/tests/test_integration.py @@ -12,7 +12,6 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, mixed_st from student.tests.factories import UserFactory from student.models import CourseEnrollment from course_modes.tests.factories import CourseModeFactory -from verify_student.models import SoftwareSecurePhotoVerification # Since we don't need any XML course fixtures, use a modulestore configuration @@ -47,82 +46,23 @@ class TestProfEdVerification(ModuleStoreTestCase): args=[unicode(self.course_key)] ), - 'verify_show_student_requirements': reverse( - 'verify_student_show_requirements', + 'verify_student_start_flow': reverse( + 'verify_student_start_flow', args=[unicode(self.course_key)] ), - - 'verify_student_verify': reverse( - 'verify_student_verify', - args=[unicode(self.course_key)] - ), - - 'verify_student_verified': reverse( - 'verify_student_verified', - args=[unicode(self.course_key)] - ) + "?upgrade=False", } - def test_new_user_flow(self): - # Go to the course mode page, expecting a redirect - # to the show requirements page - # because this is a professional ed course - # (otherwise, the student would have the option to choose their track) + 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. resp = self.client.get(self.urls['course_modes_choose'], follow=True) - self.assertRedirects(resp, self.urls['verify_show_student_requirements']) - - # On the show requirements page, verify that there's a link to the verify page - # (this is the only action the user is allowed to take) - self.assertContains(resp, self.urls['verify_student_verify']) - - # Simulate the user clicking the button by following the link - # to the verified page. - # Since there are no suggested prices for professional ed, - # expect that only one price is displayed. - resp = self.client.get(self.urls['verify_student_verify']) - self.assertEqual(self._prices_on_page(resp.content), [self.MIN_PRICE]) - - def test_already_verified_user_flow(self): - # Simulate the user already being verified - self._verify_student() - - # Go to the course mode page, expecting a redirect to the - # verified (past tense!) page. - resp = self.client.get(self.urls['course_modes_choose'], follow=True) - self.assertRedirects(resp, self.urls['verify_student_verified']) - - # Since this is a professional ed course, expect that only - # one price is shown. - self.assertContains(resp, "Your Course Total is $") - self.assertContains(resp, str(self.MIN_PRICE)) - - # On the verified page, expect that there's a link to payment page - self.assertContains(resp, '/shoppingcart/payment_fake') - - def test_do_not_auto_enroll(self): - # Go to the course mode page, expecting a redirect - # to the show requirements page. - resp = self.client.get(self.urls['course_modes_choose'], follow=True) - self.assertRedirects(resp, self.urls['verify_show_student_requirements']) + self.assertRedirects(resp, self.urls['verify_student_start_flow']) # 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)) - # Expect that the rendered page says that the student is "enrolled", - # not that they've already been enrolled. - self.assertIn("You are enrolling", resp.content) - self.assertNotIn("You are now enrolled", resp.content) - - def _prices_on_page(self, page_content): - """ Retrieve the available prices on the verify page. """ - html = soupparser.fromstring(page_content) - xpath_sel = '//li[@class="field contribution-option"]/span[@class="label-value"]/text()' - return [int(price) for price in html.xpath(xpath_sel)] - - def _verify_student(self): - """ Simulate that the student's identity has already been verified. """ - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - attempt.mark_ready() - attempt.submit() - attempt.approve() + # 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, 'pay_button') diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index 69597a756d..2967175ef1 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -1,14 +1,6 @@ # encoding: utf-8 """ - - -verify_student/start?course_id=MITx/6.002x/2013_Spring # create - /upload_face?course_id=MITx/6.002x/2013_Spring - /upload_photo_id - /confirm # mark_ready() - - ---> To Payment - +Tests of verify_student views. """ import json import mock @@ -28,7 +20,6 @@ from django.core.exceptions import ObjectDoesNotExist from django.core import mail from bs4 import BeautifulSoup -from util.testing import UrlResetMixin from openedx.core.djangoapps.user_api.api import profile as profile_api from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, mixed_store_config from xmodule.modulestore.tests.factories import CourseFactory @@ -58,13 +49,12 @@ render_mock = Mock(side_effect=mock_render_to_response) class StartView(TestCase): - def start_url(self, course_id=""): return "/verify_student/{0}".format(urllib.quote(course_id)) def test_start_new_verification(self): """ - Test the case where the user has no pending `PhotoVerficiationAttempts`, + Test the case where the user has no pending `PhotoVerificationAttempts`, but is just starting their first. """ user = UserFactory.create(username="rusty", password="test") @@ -75,897 +65,11 @@ class StartView(TestCase): @override_settings(MODULESTORE=MODULESTORE_CONFIG) -class TestCreateOrderView(ModuleStoreTestCase): - """ - Tests for the create_order view of verified course registration process - """ - - # Minimum size valid image data - IMAGE_DATA = ',' - - def setUp(self): - self.user = UserFactory.create(username="rusty", password="test") - self.client.login(username="rusty", password="test") - self.course_id = 'Robot/999/Test_Course' - self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course') - verified_mode = CourseMode( - course_id=SlashSeparatedCourseKey("Robot", "999", 'Test_Course'), - mode_slug="verified", - mode_display_name="Verified Certificate", - min_price=50 - ) - verified_mode.save() - course_mode_post_data = { - 'certificate_mode': 'Select Certificate', - 'contribution': 50, - 'contribution-other-amt': '', - 'explain': '' - } - self.client.post( - reverse("course_modes_choose", kwargs={'course_id': self.course_id}), - course_mode_post_data - ) - - def test_invalid_photos_data(self): - self._create_order( - 50, - self.course_id, - face_image='', - photo_id_image='', - expect_success=False - ) - - @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) - def test_invalid_amount(self): - response = self._create_order( - '1.a', - self.course_id, - face_image=self.IMAGE_DATA, - photo_id_image=self.IMAGE_DATA, - expect_status_code=400 - ) - self.assertIn('Selected price is not valid number.', response.content) - - @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) - def test_invalid_mode(self): - # Create a course that does not have a verified mode - course_id = 'Fake/999/Test_Course' - CourseFactory.create(org='Fake', number='999', display_name='Test Course') - response = self._create_order( - '50', - course_id, - face_image=self.IMAGE_DATA, - photo_id_image=self.IMAGE_DATA, - expect_status_code=400 - ) - self.assertIn('This course doesn\'t support verified certificates', response.content) - - @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) - def test_create_order_fail_with_get(self): - create_order_post_data = { - 'contribution': 50, - 'course_id': self.course_id, - 'face_image': self.IMAGE_DATA, - 'photo_id_image': self.IMAGE_DATA, - } - - # Use the wrong HTTP method - response = self.client.get(reverse('verify_student_create_order'), create_order_post_data) - self.assertEqual(response.status_code, 405) - - @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) - def test_create_order_success(self): - response = self._create_order( - 50, - self.course_id, - face_image=self.IMAGE_DATA, - photo_id_image=self.IMAGE_DATA - ) - json_response = json.loads(response.content) - self.assertIsNotNone(json_response.get('orderNumber')) - - # Verify that the order exists and is configured correctly - order = Order.objects.get(user=self.user) - self.assertEqual(order.status, 'paying') - item = CertificateItem.objects.get(order=order) - self.assertEqual(item.status, 'paying') - self.assertEqual(item.course_id, self.course.id) - self.assertEqual(item.mode, 'verified') - - # Verify that a photo verification attempt was created - # TODO (ECOM-188): Once the A/B test of separating verified/payment - # completes, we can delete this check. - attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user) - self.assertEqual(attempt.status, "ready") - - # TODO (ECOM-188): Once the A/B test of separating verified/payment - # completes, we can delete this test. - @patch.dict(settings.FEATURES, { - "SEPARATE_VERIFICATION_FROM_PAYMENT": True, - "AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING": True - }) - def test_create_order_skip_photo_submission(self): - self._create_order(50, self.course_id) - - # Without the face image and photo id image params, - # don't create the verification attempt. - self.assertFalse( - SoftwareSecurePhotoVerification.objects.filter(user=self.user).exists() - ) - - # Now submit *with* the params - self._create_order( - 50, self.course_id, - face_image=self.IMAGE_DATA, - photo_id_image=self.IMAGE_DATA - ) - attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user) - self.assertEqual(attempt.status, "ready") - - def _create_order( - self, contribution, course_id, - face_image=None, - photo_id_image=None, - expect_success=True, - expect_status_code=200 - ): - """Create a new order. - - Arguments: - contribution (int): The contribution amount. - course_id (CourseKey): The course to purchase. - - Keyword Arguments: - face_image (string): Base-64 encoded image data - photo_id_image (string): Base-64 encoded image data - expect_success (bool): If True, verify that the response was successful. - expect_status_code (int): The expected HTTP status code - - Returns: - HttpResponse - - """ - url = reverse('verify_student_create_order') - data = { - 'contribution': contribution, - 'course_id': course_id - } - - if face_image is not None: - data['face_image'] = face_image - if photo_id_image is not None: - data['photo_id_image'] = photo_id_image - - response = self.client.post(url, data) - self.assertEqual(response.status_code, expect_status_code) - - if expect_status_code == 200: - json_response = json.loads(response.content) - if expect_success: - self.assertTrue(json_response.get('success')) - else: - self.assertFalse(json_response.get('success')) - - return response - - -@override_settings(MODULESTORE=MODULESTORE_CONFIG) -class TestVerifyView(ModuleStoreTestCase): - def setUp(self): - self.user = UserFactory.create(username="rusty", password="test") - self.client.login(username="rusty", password="test") - self.course_key = SlashSeparatedCourseKey('Robot', '999', 'Test_Course') - self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course') - verified_mode = CourseMode(course_id=self.course_key, - mode_slug="verified", - mode_display_name="Verified Certificate", - min_price=50, - suggested_prices="50.0,100.0") - verified_mode.save() - - def test_invalid_course(self): - fake_course_id = "Robot/999/Fake_Course" - url = reverse('verify_student_verify', - kwargs={"course_id": fake_course_id}) - response = self.client.get(url) - self.assertEquals(response.status_code, 302) - - def test_valid_course_enrollment_text(self): - url = reverse('verify_student_verify', - kwargs={"course_id": unicode(self.course_key)}) - response = self.client.get(url) - self.assertIn("You are now enrolled in", response.content) - # make sure org, name, and number are present - self.assertIn(self.course.display_org_with_default, response.content) - self.assertIn(self.course.display_name_with_default, response.content) - self.assertIn(self.course.display_number_with_default, response.content) - - def test_valid_course_upgrade_text(self): - url = reverse('verify_student_verify', - kwargs={"course_id": unicode(self.course_key)}) - response = self.client.get(url, {'upgrade': "True"}) - self.assertIn("You are upgrading your enrollment for", response.content) - - def test_show_selected_contribution_amount(self): - # Set the donation amount in the client's session - session = self.client.session - session['donation_for_course'] = { - unicode(self.course_key): decimal.Decimal('1.23') - } - session.save() - - # Retrieve the page - url = reverse('verify_student_verify', kwargs={"course_id": unicode(self.course_key)}) - response = self.client.get(url) - - # Expect that the user's contribution amount is shown on the page - self.assertContains(response, '1.23') - - -@override_settings(MODULESTORE=MODULESTORE_CONFIG) -class TestVerifiedView(ModuleStoreTestCase): - """ - Tests for VerifiedView. - """ - def setUp(self): - self.user = UserFactory.create(username="abc", password="test") - self.user.profile.name = u"Røøsty Bøøgins" - self.user.save() - self.client.login(username="abc", password="test") - self.course = CourseFactory.create(org='MITx', number='999.1x', display_name='Verified Course') - self.course_id = self.course.id - - def test_verified_course_mode_none(self): - """ - Test VerifiedView when there is no active verified mode for course. - """ - url = reverse('verify_student_verified', kwargs={"course_id": self.course_id.to_deprecated_string()}) - - verify_mode = CourseMode.mode_for_course(self.course_id, "verified") - # Verify mode should be None. - self.assertEquals(verify_mode, None) - - response = self.client.get(url) - # Status code should be 302. - self.assertTrue(response.status_code, 302) - # Location should contains dashboard. - self.assertIn('dashboard', response._headers.get('location')[1]) - - def test_show_selected_contribution_amount(self): - # Configure the course to have a verified mode - for mode in ('audit', 'honor', 'verified'): - CourseModeFactory(mode_slug=mode, course_id=self.course.id) - - # Set the donation amount in the client's session - session = self.client.session - session['donation_for_course'] = { - unicode(self.course_id): decimal.Decimal('1.23') - } - session.save() - - # Retrieve the page - url = reverse('verify_student_verified', kwargs={"course_id": unicode(self.course_id)}) - response = self.client.get(url) - - # Expect that the user's contribution amount is shown on the page - self.assertContains(response, '1.23') - - -@override_settings(MODULESTORE=MODULESTORE_CONFIG) -class TestReverifyView(ModuleStoreTestCase): - """ - Tests for the reverification views - - """ - def setUp(self): - self.user = UserFactory.create(username="rusty", password="test") - self.user.profile.name = u"Røøsty Bøøgins" - self.user.profile.save() - self.client.login(username="rusty", password="test") - self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') - self.course_key = self.course.id - - @patch('verify_student.views.render_to_response', render_mock) - def test_reverify_get(self): - url = reverse('verify_student_reverify') - response = self.client.get(url) - self.assertEquals(response.status_code, 200) - ((_template, context), _kwargs) = render_mock.call_args - self.assertFalse(context['error']) - - @patch('verify_student.views.render_to_response', render_mock) - def test_reverify_post_failure(self): - url = reverse('verify_student_reverify') - response = self.client.post(url, {'face_image': '', - 'photo_id_image': ''}) - self.assertEquals(response.status_code, 200) - ((template, context), _kwargs) = render_mock.call_args - self.assertIn('photo_reverification', template) - self.assertTrue(context['error']) - - @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) - def test_reverify_post_success(self): - url = reverse('verify_student_reverify') - response = self.client.post(url, {'face_image': ',', - 'photo_id_image': ','}) - self.assertEquals(response.status_code, 302) - try: - verification_attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user) - self.assertIsNotNone(verification_attempt) - except ObjectDoesNotExist: - self.fail('No verification object generated') - ((template, context), _kwargs) = render_mock.call_args - self.assertIn('photo_reverification', template) - self.assertTrue(context['error']) - - -@override_settings(MODULESTORE=MODULESTORE_CONFIG) -class TestPhotoVerificationResultsCallback(ModuleStoreTestCase): - """ - Tests for the results_callback view. - """ - def setUp(self): - self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course') - self.course_id = self.course.id - self.user = UserFactory.create() - self.attempt = SoftwareSecurePhotoVerification( - status="submitted", - user=self.user - ) - self.attempt.save() - self.receipt_id = self.attempt.receipt_id - self.client = Client() - - def mocked_has_valid_signature(method, headers_dict, body_dict, access_key, secret_key): - return True - - def test_invalid_json(self): - """ - Test for invalid json being posted by software secure. - """ - data = {"Testing invalid"} - response = self.client.post( - reverse('verify_student_results_callback'), - data=data, - content_type='application/json', - HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB: testing', - HTTP_DATE='testdate' - ) - self.assertIn('Invalid JSON', response.content) - self.assertEqual(response.status_code, 400) - - def test_invalid_dict(self): - """ - Test for invalid dictionary being posted by software secure. - """ - data = '"\\"Test\\tTesting"' - response = self.client.post( - reverse('verify_student_results_callback'), - data=data, - content_type='application/json', - HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', - HTTP_DATE='testdate' - ) - self.assertIn('JSON should be dict', response.content) - self.assertEqual(response.status_code, 400) - - @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature)) - def test_invalid_access_key(self): - """ - Test for invalid access key. - """ - data = { - "EdX-ID": self.receipt_id, - "Result": "Testing", - "Reason": "Testing", - "MessageType": "Testing" - } - json_data = json.dumps(data) - response = self.client.post( - reverse('verify_student_results_callback'), - data=json_data, - content_type='application/json', - HTTP_AUTHORIZATION='test testing:testing', - HTTP_DATE='testdate' - ) - self.assertIn('Access key invalid', response.content) - self.assertEqual(response.status_code, 400) - - @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature)) - def test_wrong_edx_id(self): - """ - Test for wrong id of Software secure verification attempt. - """ - data = { - "EdX-ID": "Invalid-Id", - "Result": "Testing", - "Reason": "Testing", - "MessageType": "Testing" - } - json_data = json.dumps(data) - response = self.client.post( - reverse('verify_student_results_callback'), - data=json_data, - content_type='application/json', - HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', - HTTP_DATE='testdate' - ) - self.assertIn('edX ID Invalid-Id not found', response.content) - self.assertEqual(response.status_code, 400) - - @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature)) - def test_pass_result(self): - """ - Test for verification passed. - """ - data = { - "EdX-ID": self.receipt_id, - "Result": "PASS", - "Reason": "", - "MessageType": "You have been verified." - } - json_data = json.dumps(data) - response = self.client.post( - reverse('verify_student_results_callback'), data=json_data, - content_type='application/json', - HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', - HTTP_DATE='testdate' - ) - attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id) - self.assertEqual(attempt.status, u'approved') - self.assertEquals(response.content, 'OK!') - - @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature)) - def test_fail_result(self): - """ - Test for failed verification. - """ - data = { - "EdX-ID": self.receipt_id, - "Result": 'FAIL', - "Reason": 'Invalid photo', - "MessageType": 'Your photo doesn\'t meet standards.' - } - json_data = json.dumps(data) - response = self.client.post( - reverse('verify_student_results_callback'), - data=json_data, - content_type='application/json', - HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', - HTTP_DATE='testdate' - ) - attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id) - self.assertEqual(attempt.status, u'denied') - self.assertEqual(attempt.error_code, u'Your photo doesn\'t meet standards.') - self.assertEqual(attempt.error_msg, u'"Invalid photo"') - self.assertEquals(response.content, 'OK!') - - @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature)) - def test_system_fail_result(self): - """ - Test for software secure result system failure. - """ - data = {"EdX-ID": self.receipt_id, - "Result": 'SYSTEM FAIL', - "Reason": 'Memory overflow', - "MessageType": 'You must retry the verification.'} - json_data = json.dumps(data) - response = self.client.post( - reverse('verify_student_results_callback'), - data=json_data, - content_type='application/json', - HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', - HTTP_DATE='testdate' - ) - attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id) - self.assertEqual(attempt.status, u'must_retry') - self.assertEqual(attempt.error_code, u'You must retry the verification.') - self.assertEqual(attempt.error_msg, u'"Memory overflow"') - self.assertEquals(response.content, 'OK!') - - @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature)) - def test_unknown_result(self): - """ - test for unknown software secure result - """ - data = { - "EdX-ID": self.receipt_id, - "Result": 'Unknown', - "Reason": 'Unknown reason', - "MessageType": 'Unknown message' - } - json_data = json.dumps(data) - response = self.client.post( - reverse('verify_student_results_callback'), - data=json_data, - content_type='application/json', - HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', - HTTP_DATE='testdate' - ) - self.assertIn('Result Unknown not understood', response.content) - - @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature)) - def test_reverification(self): - """ - Test software secure result for reverification window. - """ - data = { - "EdX-ID": self.receipt_id, - "Result": "PASS", - "Reason": "", - "MessageType": "You have been verified." - } - window = MidcourseReverificationWindowFactory(course_id=self.course_id) - self.attempt.window = window - self.attempt.save() - json_data = json.dumps(data) - self.assertEqual(CourseEnrollment.objects.filter(course_id=self.course_id).count(), 0) - response = self.client.post( - reverse('verify_student_results_callback'), - data=json_data, - content_type='application/json', - HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', - HTTP_DATE='testdate' - ) - self.assertEquals(response.content, 'OK!') - self.assertIsNotNone(CourseEnrollment.objects.get(course_id=self.course_id)) - - -@override_settings(MODULESTORE=MODULESTORE_CONFIG) -class TestMidCourseReverifyView(ModuleStoreTestCase): - """ Tests for the midcourse reverification views """ - def setUp(self): - self.user = UserFactory.create(username="rusty", password="test") - self.client.login(username="rusty", password="test") - self.course_key = SlashSeparatedCourseKey("Robot", "999", "Test_Course") - CourseFactory.create(org='Robot', number='999', display_name='Test Course') - - patcher = patch('student.models.tracker') - self.mock_tracker = patcher.start() - self.addCleanup(patcher.stop) - - @patch('verify_student.views.render_to_response', render_mock) - def test_midcourse_reverify_get(self): - url = reverse('verify_student_midcourse_reverify', - kwargs={"course_id": self.course_key.to_deprecated_string()}) - response = self.client.get(url) - - self.mock_tracker.emit.assert_any_call( # pylint: disable=maybe-no-member - 'edx.course.enrollment.mode_changed', - { - 'user_id': self.user.id, - 'course_id': self.course_key.to_deprecated_string(), - 'mode': "verified", - } - ) - - # Check that user entering the reverify flow was logged, and that it was the last call - self.mock_tracker.emit.assert_called_with( # pylint: disable=maybe-no-member - 'edx.course.enrollment.reverify.started', - { - 'user_id': self.user.id, - 'course_id': self.course_key.to_deprecated_string(), - 'mode': "verified", - } - ) - - self.assertTrue(self.mock_tracker.emit.call_count, 2) - - self.mock_tracker.emit.reset_mock() # pylint: disable=maybe-no-member - - self.assertEquals(response.status_code, 200) - ((_template, context), _kwargs) = render_mock.call_args - self.assertFalse(context['error']) - - @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) - def test_midcourse_reverify_post_success(self): - window = MidcourseReverificationWindowFactory(course_id=self.course_key) - url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_key.to_deprecated_string()}) - - response = self.client.post(url, {'face_image': ','}) - - self.mock_tracker.emit.assert_any_call( # pylint: disable=maybe-no-member - 'edx.course.enrollment.mode_changed', - { - 'user_id': self.user.id, - 'course_id': self.course_key.to_deprecated_string(), - 'mode': "verified", - } - ) - - # Check that submission event was logged, and that it was the last call - self.mock_tracker.emit.assert_called_with( # pylint: disable=maybe-no-member - 'edx.course.enrollment.reverify.submitted', - { - 'user_id': self.user.id, - 'course_id': self.course_key.to_deprecated_string(), - 'mode': "verified", - } - ) - - self.assertTrue(self.mock_tracker.emit.call_count, 2) - - self.mock_tracker.emit.reset_mock() # pylint: disable=maybe-no-member - - self.assertEquals(response.status_code, 302) - try: - verification_attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user, window=window) - self.assertIsNotNone(verification_attempt) - except ObjectDoesNotExist: - self.fail('No verification object generated') - - @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) - def test_midcourse_reverify_post_failure_expired_window(self): - window = MidcourseReverificationWindowFactory( - course_id=self.course_key, - start_date=datetime.now(pytz.UTC) - timedelta(days=100), - end_date=datetime.now(pytz.UTC) - timedelta(days=50), - ) - url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_key.to_deprecated_string()}) - response = self.client.post(url, {'face_image': ','}) - self.assertEquals(response.status_code, 302) - with self.assertRaises(ObjectDoesNotExist): - SoftwareSecurePhotoVerification.objects.get(user=self.user, window=window) - - @patch('verify_student.views.render_to_response', render_mock) - def test_midcourse_reverify_dash(self): - url = reverse('verify_student_midcourse_reverify_dash') - response = self.client.get(url) - # not enrolled in any courses - self.assertEquals(response.status_code, 200) - - enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course_key) - enrollment.update_enrollment(mode="verified", is_active=True) - MidcourseReverificationWindowFactory(course_id=self.course_key) - response = self.client.get(url) - # enrolled in a verified course, and the window is open - self.assertEquals(response.status_code, 200) - - @patch('verify_student.views.render_to_response', render_mock) - def test_midcourse_reverify_invalid_course_id(self): - # if course id is invalid return 400 - invalid_course_key = CourseLocator('edx', 'not', 'valid') - url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': unicode(invalid_course_key)}) - response = self.client.get(url) - self.assertEqual(response.status_code, 404) - - -@override_settings(MODULESTORE=MODULESTORE_CONFIG) -class TestReverificationBanner(ModuleStoreTestCase): - """ Tests for the midcourse reverification failed toggle banner off """ - - @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) - def setUp(self): - self.user = UserFactory.create(username="rusty", password="test") - self.client.login(username="rusty", password="test") - self.course_id = 'Robot/999/Test_Course' - CourseFactory.create(org='Robot', number='999', display_name=u'Test Course é') - self.window = MidcourseReverificationWindowFactory(course_id=self.course_id) - url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_id}) - self.client.post(url, {'face_image': ','}) - photo_verification = SoftwareSecurePhotoVerification.objects.get(user=self.user, window=self.window) - photo_verification.status = 'denied' - photo_verification.save() - - def test_banner_display_off(self): - self.client.post(reverse('verify_student_toggle_failed_banner_off')) - photo_verification = SoftwareSecurePhotoVerification.objects.get(user=self.user, window=self.window) - self.assertFalse(photo_verification.display) - - -@override_settings(MODULESTORE=MODULESTORE_CONFIG) -class TestCreateOrder(ModuleStoreTestCase): - """ Tests for the create order view. """ - - def setUp(self): - """ Create a user and course. """ - self.user = UserFactory.create(username="test", password="test") - self.course = CourseFactory.create() - for mode in ('audit', 'honor', 'verified'): - CourseModeFactory(mode_slug=mode, course_id=self.course.id) - self.client.login(username="test", password="test") - - def test_create_order_already_verified(self): - # Verify the student so we don't need to submit photos - self._verify_student() - - # Create an order - url = reverse('verify_student_create_order') - params = { - 'course_id': unicode(self.course.id), - } - response = self.client.post(url, params) - self.assertEqual(response.status_code, 200) - - # Verify that the information will be sent to the correct callback URL - # (configured by test settings) - data = json.loads(response.content) - self.assertEqual(data['override_custom_receipt_page'], "http://testserver/shoppingcart/postpay_callback/") - - # Verify that the course ID and transaction type are included in "merchant-defined data" - self.assertEqual(data['merchant_defined_data1'], unicode(self.course.id)) - self.assertEqual(data['merchant_defined_data2'], "verified") - - def test_create_order_already_verified_prof_ed(self): - # Verify the student so we don't need to submit photos - self._verify_student() - - # Create a prof ed course - course = CourseFactory.create() - CourseModeFactory(mode_slug="professional", course_id=course.id) - - # Create an order for a prof ed course - url = reverse('verify_student_create_order') - params = { - 'course_id': unicode(course.id) - } - response = self.client.post(url, params) - self.assertEqual(response.status_code, 200) - - # Verify that the course ID and transaction type are included in "merchant-defined data" - data = json.loads(response.content) - self.assertEqual(data['merchant_defined_data1'], unicode(course.id)) - self.assertEqual(data['merchant_defined_data2'], "professional") - - def test_create_order_set_donation_amount(self): - # Verify the student so we don't need to submit photos - self._verify_student() - - # Create an order - url = reverse('verify_student_create_order') - params = { - 'course_id': unicode(self.course.id), - 'contribution': '1.23' - } - self.client.post(url, params) - - # Verify that the client's session contains the new donation amount - self.assertIn('donation_for_course', self.client.session) - self.assertIn(unicode(self.course.id), self.client.session['donation_for_course']) - - actual_amount = self.client.session['donation_for_course'][unicode(self.course.id)] - expected_amount = decimal.Decimal('1.23') - self.assertEqual(actual_amount, expected_amount) - - def _verify_student(self): - """ Simulate that the student's identity has already been verified. """ - attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) - attempt.mark_ready() - attempt.submit() - attempt.approve() - - @ddt.ddt -@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) -class TestSubmitPhotosForVerification(UrlResetMixin, TestCase): - """Tests for submitting photos for verification. """ - - USERNAME = "test_user" - PASSWORD = "test_password" - IMAGE_DATA = "abcd,1234" - FULL_NAME = u"Ḟüḷḷ Ṅäṁë" - - @patch.dict(settings.FEATURES, {'SEPARATE_VERIFICATION_FROM_PAYMENT': True}) - def setUp(self): - super(TestSubmitPhotosForVerification, self).setUp('verify_student.urls') - self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD) - result = self.client.login(username=self.USERNAME, password=self.PASSWORD) - self.assertTrue(result, msg="Could not log in") - - def test_submit_photos(self): - # Submit the photos - self._submit_photos( - face_image=self.IMAGE_DATA, - photo_id_image=self.IMAGE_DATA - ) - - # Verify that the attempt is created in the database - attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user) - self.assertEqual(attempt.status, "submitted") - - # Verify that the user's name wasn't changed - self._assert_full_name(self.user.profile.name) - - def test_submit_photos_and_change_name(self): - # Submit the photos, along with a name change - self._submit_photos( - face_image=self.IMAGE_DATA, - photo_id_image=self.IMAGE_DATA, - full_name=self.FULL_NAME - ) - - # Check that the user's name was changed in the database - self._assert_full_name(self.FULL_NAME) - - @ddt.data('face_image', 'photo_id_image') - def test_invalid_image_data(self, invalid_param): - params = { - 'face_image': self.IMAGE_DATA, - 'photo_id_image': self.IMAGE_DATA - } - params[invalid_param] = "" - response = self._submit_photos(expected_status_code=400, **params) - self.assertEqual(response.content, "Image data is not valid.") - - def test_invalid_name(self): - response = self._submit_photos( - face_image=self.IMAGE_DATA, - photo_id_image=self.IMAGE_DATA, - full_name="a", - expected_status_code=400 - ) - self.assertEqual(response.content, "Name must be at least 2 characters long.") - - @ddt.data('face_image', 'photo_id_image') - def test_missing_required_params(self, missing_param): - params = { - 'face_image': self.IMAGE_DATA, - 'photo_id_image': self.IMAGE_DATA - } - del params[missing_param] - response = self._submit_photos(expected_status_code=400, **params) - self.assertEqual( - response.content, - "Missing required parameters: {missing}".format(missing=missing_param) - ) - - def _submit_photos(self, face_image=None, photo_id_image=None, full_name=None, expected_status_code=200): - """Submit photos for verification. - - Keyword Arguments: - face_image (str): The base-64 encoded face image data. - photo_id_image (str): The base-64 encoded ID image data. - full_name (unicode): The full name of the user, if the user is changing it. - expected_status_code (int): The expected response status code. - - Returns: - HttpResponse - - """ - url = reverse("verify_student_submit_photos") - params = {} - - if face_image is not None: - params['face_image'] = face_image - - if photo_id_image is not None: - params['photo_id_image'] = photo_id_image - - if full_name is not None: - params['full_name'] = full_name - - response = self.client.post(url, params) - self.assertEqual(response.status_code, expected_status_code) - - if expected_status_code == 200: - # Verify that photo submission confirmation email was sent - self.assertEqual(len(mail.outbox), 1) - self.assertEqual("Verification photos received", mail.outbox[0].subject) - else: - # Verify that photo submission confirmation email was not sent - self.assertEqual(len(mail.outbox), 0) - - return response - - def _assert_full_name(self, full_name): - """Check the user's full name. - - Arguments: - full_name (unicode): The user's full name. - - Raises: - AssertionError - - """ - info = profile_api.profile_info(self.user.username) - self.assertEqual(info['full_name'], full_name) - - -@override_settings(MODULESTORE=MODULESTORE_CONFIG) -@ddt.ddt -class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase): - """Tests for the payment / verification flow views. """ - +class TestPayAndVerifyView(ModuleStoreTestCase): + """ + Tests for the payment and verification flow views. + """ MIN_PRICE = 12 USERNAME = "test_user" PASSWORD = "test_password" @@ -974,9 +78,8 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase): YESTERDAY = NOW - timedelta(days=1) TOMORROW = NOW + timedelta(days=1) - @patch.dict(settings.FEATURES, {'SEPARATE_VERIFICATION_FROM_PAYMENT': True}) def setUp(self): - super(TestPayAndVerifyView, self).setUp('verify_student.urls') + super(TestPayAndVerifyView, self).setUp() self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD) result = self.client.login(username=self.USERNAME, password=self.PASSWORD) self.assertTrue(result, msg="Could not log in") @@ -1664,3 +767,764 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase): """Check that the page redirects to the "upgrade" part of the flow. """ url = reverse('verify_student_upgrade_and_verify', kwargs={'course_id': unicode(course_id)}) self.assertRedirects(response, url) + + +@override_settings(MODULESTORE=MODULESTORE_CONFIG) +class TestCreateOrder(ModuleStoreTestCase): + """ + Tests for the create order view. + """ + def setUp(self): + """ Create a user and course. """ + self.user = UserFactory.create(username="test", password="test") + self.course = CourseFactory.create() + for mode in ('audit', 'honor', 'verified'): + CourseModeFactory(mode_slug=mode, course_id=self.course.id) + self.client.login(username="test", password="test") + + def test_create_order_already_verified(self): + # Verify the student so we don't need to submit photos + self._verify_student() + + # Create an order + url = reverse('verify_student_create_order') + params = { + 'course_id': unicode(self.course.id), + } + response = self.client.post(url, params) + self.assertEqual(response.status_code, 200) + + # Verify that the information will be sent to the correct callback URL + # (configured by test settings) + data = json.loads(response.content) + self.assertEqual(data['override_custom_receipt_page'], "http://testserver/shoppingcart/postpay_callback/") + + # Verify that the course ID and transaction type are included in "merchant-defined data" + self.assertEqual(data['merchant_defined_data1'], unicode(self.course.id)) + self.assertEqual(data['merchant_defined_data2'], "verified") + + def test_create_order_already_verified_prof_ed(self): + # Verify the student so we don't need to submit photos + self._verify_student() + + # Create a prof ed course + course = CourseFactory.create() + CourseModeFactory(mode_slug="professional", course_id=course.id) + + # Create an order for a prof ed course + url = reverse('verify_student_create_order') + params = { + 'course_id': unicode(course.id) + } + response = self.client.post(url, params) + self.assertEqual(response.status_code, 200) + + # Verify that the course ID and transaction type are included in "merchant-defined data" + data = json.loads(response.content) + self.assertEqual(data['merchant_defined_data1'], unicode(course.id)) + self.assertEqual(data['merchant_defined_data2'], "professional") + + def test_create_order_set_donation_amount(self): + # Verify the student so we don't need to submit photos + self._verify_student() + + # Create an order + url = reverse('verify_student_create_order') + params = { + 'course_id': unicode(self.course.id), + 'contribution': '1.23' + } + self.client.post(url, params) + + # Verify that the client's session contains the new donation amount + self.assertIn('donation_for_course', self.client.session) + self.assertIn(unicode(self.course.id), self.client.session['donation_for_course']) + + actual_amount = self.client.session['donation_for_course'][unicode(self.course.id)] + expected_amount = decimal.Decimal('1.23') + self.assertEqual(actual_amount, expected_amount) + + def _verify_student(self): + """ Simulate that the student's identity has already been verified. """ + attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user) + attempt.mark_ready() + attempt.submit() + attempt.approve() + + +@override_settings(MODULESTORE=MODULESTORE_CONFIG) +class TestCreateOrderView(ModuleStoreTestCase): + """ + Tests for the create_order view of verified course enrollment process. + """ + # Minimum size valid image data + IMAGE_DATA = ',' + + def setUp(self): + self.user = UserFactory.create(username="rusty", password="test") + self.client.login(username="rusty", password="test") + self.course_id = 'Robot/999/Test_Course' + self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course') + verified_mode = CourseMode( + course_id=SlashSeparatedCourseKey("Robot", "999", 'Test_Course'), + mode_slug="verified", + mode_display_name="Verified Certificate", + min_price=50 + ) + verified_mode.save() + course_mode_post_data = { + 'certificate_mode': 'Select Certificate', + 'contribution': 50, + 'contribution-other-amt': '', + 'explain': '' + } + self.client.post( + reverse("course_modes_choose", kwargs={'course_id': self.course_id}), + course_mode_post_data + ) + + def test_invalid_photos_data(self): + self._create_order( + 50, + self.course_id, + face_image='', + photo_id_image='', + expect_success=False + ) + + @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) + def test_invalid_amount(self): + response = self._create_order( + '1.a', + self.course_id, + face_image=self.IMAGE_DATA, + photo_id_image=self.IMAGE_DATA, + expect_status_code=400 + ) + self.assertIn('Selected price is not valid number.', response.content) + + @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) + def test_invalid_mode(self): + # Create a course that does not have a verified mode + course_id = 'Fake/999/Test_Course' + CourseFactory.create(org='Fake', number='999', display_name='Test Course') + response = self._create_order( + '50', + course_id, + face_image=self.IMAGE_DATA, + photo_id_image=self.IMAGE_DATA, + expect_status_code=400 + ) + self.assertIn('This course doesn\'t support verified certificates', response.content) + + @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) + def test_create_order_fail_with_get(self): + create_order_post_data = { + 'contribution': 50, + 'course_id': self.course_id, + 'face_image': self.IMAGE_DATA, + 'photo_id_image': self.IMAGE_DATA, + } + + # Use the wrong HTTP method + response = self.client.get(reverse('verify_student_create_order'), create_order_post_data) + self.assertEqual(response.status_code, 405) + + @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) + def test_create_order_success(self): + response = self._create_order( + 50, + self.course_id, + face_image=self.IMAGE_DATA, + photo_id_image=self.IMAGE_DATA + ) + json_response = json.loads(response.content) + self.assertIsNotNone(json_response.get('orderNumber')) + + # Verify that the order exists and is configured correctly + order = Order.objects.get(user=self.user) + self.assertEqual(order.status, 'paying') + item = CertificateItem.objects.get(order=order) + self.assertEqual(item.status, 'paying') + self.assertEqual(item.course_id, self.course.id) + self.assertEqual(item.mode, 'verified') + + def _create_order( + self, contribution, course_id, + face_image=None, + photo_id_image=None, + expect_success=True, + expect_status_code=200 + ): + """Create a new order. + + Arguments: + contribution (int): The contribution amount. + course_id (CourseKey): The course to purchase. + + Keyword Arguments: + face_image (string): Base-64 encoded image data + photo_id_image (string): Base-64 encoded image data + expect_success (bool): If True, verify that the response was successful. + expect_status_code (int): The expected HTTP status code + + Returns: + HttpResponse + + """ + url = reverse('verify_student_create_order') + data = { + 'contribution': contribution, + 'course_id': course_id + } + + if face_image is not None: + data['face_image'] = face_image + if photo_id_image is not None: + data['photo_id_image'] = photo_id_image + + response = self.client.post(url, data) + self.assertEqual(response.status_code, expect_status_code) + + if expect_status_code == 200: + json_response = json.loads(response.content) + if expect_success: + self.assertTrue(json_response.get('success')) + else: + self.assertFalse(json_response.get('success')) + + return response + + +@ddt.ddt +@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) +class TestSubmitPhotosForVerification(TestCase): + """ + Tests for submitting photos for verification. + """ + USERNAME = "test_user" + PASSWORD = "test_password" + IMAGE_DATA = "abcd,1234" + FULL_NAME = u"Ḟüḷḷ Ṅäṁë" + + def setUp(self): + super(TestSubmitPhotosForVerification, self).setUp() + self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD) + result = self.client.login(username=self.USERNAME, password=self.PASSWORD) + self.assertTrue(result, msg="Could not log in") + + def test_submit_photos(self): + # Submit the photos + self._submit_photos( + face_image=self.IMAGE_DATA, + photo_id_image=self.IMAGE_DATA + ) + + # Verify that the attempt is created in the database + attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user) + self.assertEqual(attempt.status, "submitted") + + # Verify that the user's name wasn't changed + self._assert_full_name(self.user.profile.name) + + def test_submit_photos_and_change_name(self): + # Submit the photos, along with a name change + self._submit_photos( + face_image=self.IMAGE_DATA, + photo_id_image=self.IMAGE_DATA, + full_name=self.FULL_NAME + ) + + # Check that the user's name was changed in the database + self._assert_full_name(self.FULL_NAME) + + @ddt.data('face_image', 'photo_id_image') + def test_invalid_image_data(self, invalid_param): + params = { + 'face_image': self.IMAGE_DATA, + 'photo_id_image': self.IMAGE_DATA + } + params[invalid_param] = "" + response = self._submit_photos(expected_status_code=400, **params) + self.assertEqual(response.content, "Image data is not valid.") + + def test_invalid_name(self): + response = self._submit_photos( + face_image=self.IMAGE_DATA, + photo_id_image=self.IMAGE_DATA, + full_name="a", + expected_status_code=400 + ) + self.assertEqual(response.content, "Name must be at least 2 characters long.") + + @ddt.data('face_image', 'photo_id_image') + def test_missing_required_params(self, missing_param): + params = { + 'face_image': self.IMAGE_DATA, + 'photo_id_image': self.IMAGE_DATA + } + del params[missing_param] + response = self._submit_photos(expected_status_code=400, **params) + self.assertEqual( + response.content, + "Missing required parameters: {missing}".format(missing=missing_param) + ) + + def _submit_photos(self, face_image=None, photo_id_image=None, full_name=None, expected_status_code=200): + """Submit photos for verification. + + Keyword Arguments: + face_image (str): The base-64 encoded face image data. + photo_id_image (str): The base-64 encoded ID image data. + full_name (unicode): The full name of the user, if the user is changing it. + expected_status_code (int): The expected response status code. + + Returns: + HttpResponse + + """ + url = reverse("verify_student_submit_photos") + params = {} + + if face_image is not None: + params['face_image'] = face_image + + if photo_id_image is not None: + params['photo_id_image'] = photo_id_image + + if full_name is not None: + params['full_name'] = full_name + + response = self.client.post(url, params) + self.assertEqual(response.status_code, expected_status_code) + + if expected_status_code == 200: + # Verify that photo submission confirmation email was sent + self.assertEqual(len(mail.outbox), 1) + self.assertEqual("Verification photos received", mail.outbox[0].subject) + else: + # Verify that photo submission confirmation email was not sent + self.assertEqual(len(mail.outbox), 0) + + return response + + def _assert_full_name(self, full_name): + """Check the user's full name. + + Arguments: + full_name (unicode): The user's full name. + + Raises: + AssertionError + + """ + info = profile_api.profile_info(self.user.username) + self.assertEqual(info['full_name'], full_name) + + +@override_settings(MODULESTORE=MODULESTORE_CONFIG) +class TestPhotoVerificationResultsCallback(ModuleStoreTestCase): + """ + Tests for the results_callback view. + """ + def setUp(self): + self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course') + self.course_id = self.course.id + self.user = UserFactory.create() + self.attempt = SoftwareSecurePhotoVerification( + status="submitted", + user=self.user + ) + self.attempt.save() + self.receipt_id = self.attempt.receipt_id + self.client = Client() + + def mocked_has_valid_signature(method, headers_dict, body_dict, access_key, secret_key): # pylint: disable=no-self-argument, unused-argument + """ + Used as a side effect when mocking `verify_student.ssencrypt.has_valid_signature`. + """ + return True + + def test_invalid_json(self): + """ + Test for invalid json being posted by software secure. + """ + data = {"Testing invalid"} + response = self.client.post( + reverse('verify_student_results_callback'), + data=data, + content_type='application/json', + HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB: testing', + HTTP_DATE='testdate' + ) + self.assertIn('Invalid JSON', response.content) + self.assertEqual(response.status_code, 400) + + def test_invalid_dict(self): + """ + Test for invalid dictionary being posted by software secure. + """ + data = '"\\"Test\\tTesting"' + response = self.client.post( + reverse('verify_student_results_callback'), + data=data, + content_type='application/json', + HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', + HTTP_DATE='testdate' + ) + self.assertIn('JSON should be dict', response.content) + self.assertEqual(response.status_code, 400) + + @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature)) + def test_invalid_access_key(self): + """ + Test for invalid access key. + """ + data = { + "EdX-ID": self.receipt_id, + "Result": "Testing", + "Reason": "Testing", + "MessageType": "Testing" + } + json_data = json.dumps(data) + response = self.client.post( + reverse('verify_student_results_callback'), + data=json_data, + content_type='application/json', + HTTP_AUTHORIZATION='test testing:testing', + HTTP_DATE='testdate' + ) + self.assertIn('Access key invalid', response.content) + self.assertEqual(response.status_code, 400) + + @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature)) + def test_wrong_edx_id(self): + """ + Test for wrong id of Software secure verification attempt. + """ + data = { + "EdX-ID": "Invalid-Id", + "Result": "Testing", + "Reason": "Testing", + "MessageType": "Testing" + } + json_data = json.dumps(data) + response = self.client.post( + reverse('verify_student_results_callback'), + data=json_data, + content_type='application/json', + HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', + HTTP_DATE='testdate' + ) + self.assertIn('edX ID Invalid-Id not found', response.content) + self.assertEqual(response.status_code, 400) + + @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature)) + def test_pass_result(self): + """ + Test for verification passed. + """ + data = { + "EdX-ID": self.receipt_id, + "Result": "PASS", + "Reason": "", + "MessageType": "You have been verified." + } + json_data = json.dumps(data) + response = self.client.post( + reverse('verify_student_results_callback'), data=json_data, + content_type='application/json', + HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', + HTTP_DATE='testdate' + ) + attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id) + self.assertEqual(attempt.status, u'approved') + self.assertEquals(response.content, 'OK!') + + @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature)) + def test_fail_result(self): + """ + Test for failed verification. + """ + data = { + "EdX-ID": self.receipt_id, + "Result": 'FAIL', + "Reason": 'Invalid photo', + "MessageType": 'Your photo doesn\'t meet standards.' + } + json_data = json.dumps(data) + response = self.client.post( + reverse('verify_student_results_callback'), + data=json_data, + content_type='application/json', + HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', + HTTP_DATE='testdate' + ) + attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id) + self.assertEqual(attempt.status, u'denied') + self.assertEqual(attempt.error_code, u'Your photo doesn\'t meet standards.') + self.assertEqual(attempt.error_msg, u'"Invalid photo"') + self.assertEquals(response.content, 'OK!') + + @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature)) + def test_system_fail_result(self): + """ + Test for software secure result system failure. + """ + data = {"EdX-ID": self.receipt_id, + "Result": 'SYSTEM FAIL', + "Reason": 'Memory overflow', + "MessageType": 'You must retry the verification.'} + json_data = json.dumps(data) + response = self.client.post( + reverse('verify_student_results_callback'), + data=json_data, + content_type='application/json', + HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', + HTTP_DATE='testdate' + ) + attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id) + self.assertEqual(attempt.status, u'must_retry') + self.assertEqual(attempt.error_code, u'You must retry the verification.') + self.assertEqual(attempt.error_msg, u'"Memory overflow"') + self.assertEquals(response.content, 'OK!') + + @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature)) + def test_unknown_result(self): + """ + test for unknown software secure result + """ + data = { + "EdX-ID": self.receipt_id, + "Result": 'Unknown', + "Reason": 'Unknown reason', + "MessageType": 'Unknown message' + } + json_data = json.dumps(data) + response = self.client.post( + reverse('verify_student_results_callback'), + data=json_data, + content_type='application/json', + HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', + HTTP_DATE='testdate' + ) + self.assertIn('Result Unknown not understood', response.content) + + @mock.patch('verify_student.ssencrypt.has_valid_signature', mock.Mock(side_effect=mocked_has_valid_signature)) + def test_reverification(self): + """ + Test software secure result for reverification window. + """ + data = { + "EdX-ID": self.receipt_id, + "Result": "PASS", + "Reason": "", + "MessageType": "You have been verified." + } + window = MidcourseReverificationWindowFactory(course_id=self.course_id) + self.attempt.window = window + self.attempt.save() + json_data = json.dumps(data) + self.assertEqual(CourseEnrollment.objects.filter(course_id=self.course_id).count(), 0) + response = self.client.post( + reverse('verify_student_results_callback'), + data=json_data, + content_type='application/json', + HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing', + HTTP_DATE='testdate' + ) + self.assertEquals(response.content, 'OK!') + self.assertIsNotNone(CourseEnrollment.objects.get(course_id=self.course_id)) + + +@override_settings(MODULESTORE=MODULESTORE_CONFIG) +class TestReverifyView(ModuleStoreTestCase): + """ + Tests for the reverification views + """ + def setUp(self): + self.user = UserFactory.create(username="rusty", password="test") + self.user.profile.name = u"Røøsty Bøøgins" + self.user.profile.save() + self.client.login(username="rusty", password="test") + self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') + self.course_key = self.course.id + + @patch('verify_student.views.render_to_response', render_mock) + def test_reverify_get(self): + url = reverse('verify_student_reverify') + response = self.client.get(url) + self.assertEquals(response.status_code, 200) + ((_template, context), _kwargs) = render_mock.call_args # pylint: disable=unpacking-non-sequence + self.assertFalse(context['error']) + + @patch('verify_student.views.render_to_response', render_mock) + def test_reverify_post_failure(self): + url = reverse('verify_student_reverify') + response = self.client.post(url, {'face_image': '', + 'photo_id_image': ''}) + self.assertEquals(response.status_code, 200) + ((template, context), _kwargs) = render_mock.call_args # pylint: disable=unpacking-non-sequence + self.assertIn('photo_reverification', template) + self.assertTrue(context['error']) + + @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) + def test_reverify_post_success(self): + url = reverse('verify_student_reverify') + response = self.client.post(url, {'face_image': ',', + 'photo_id_image': ','}) + self.assertEquals(response.status_code, 302) + try: + verification_attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user) + self.assertIsNotNone(verification_attempt) + except ObjectDoesNotExist: + self.fail('No verification object generated') + ((template, context), _kwargs) = render_mock.call_args # pylint: disable=unpacking-non-sequence + self.assertIn('photo_reverification', template) + self.assertTrue(context['error']) + + +@override_settings(MODULESTORE=MODULESTORE_CONFIG) +class TestMidCourseReverifyView(ModuleStoreTestCase): + """ + Tests for the midcourse reverification views. + """ + def setUp(self): + self.user = UserFactory.create(username="rusty", password="test") + self.client.login(username="rusty", password="test") + self.course_key = SlashSeparatedCourseKey("Robot", "999", "Test_Course") + CourseFactory.create(org='Robot', number='999', display_name='Test Course') + + patcher = patch('student.models.tracker') + self.mock_tracker = patcher.start() + self.addCleanup(patcher.stop) + + @patch('verify_student.views.render_to_response', render_mock) + def test_midcourse_reverify_get(self): + url = reverse('verify_student_midcourse_reverify', + kwargs={"course_id": self.course_key.to_deprecated_string()}) + response = self.client.get(url) + + self.mock_tracker.emit.assert_any_call( # pylint: disable=maybe-no-member + 'edx.course.enrollment.mode_changed', + { + 'user_id': self.user.id, + 'course_id': self.course_key.to_deprecated_string(), + 'mode': "verified", + } + ) + + # Check that user entering the reverify flow was logged, and that it was the last call + self.mock_tracker.emit.assert_called_with( # pylint: disable=maybe-no-member + 'edx.course.enrollment.reverify.started', + { + 'user_id': self.user.id, + 'course_id': self.course_key.to_deprecated_string(), + 'mode': "verified", + } + ) + + self.assertTrue(self.mock_tracker.emit.call_count, 2) # pylint: disable=no-member + + self.mock_tracker.emit.reset_mock() # pylint: disable=no-member + + self.assertEquals(response.status_code, 200) + ((_template, context), _kwargs) = render_mock.call_args # pylint: disable=unpacking-non-sequence + self.assertFalse(context['error']) + + @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) + def test_midcourse_reverify_post_success(self): + window = MidcourseReverificationWindowFactory(course_id=self.course_key) + url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_key.to_deprecated_string()}) + + response = self.client.post(url, {'face_image': ','}) + + self.mock_tracker.emit.assert_any_call( # pylint: disable=maybe-no-member + 'edx.course.enrollment.mode_changed', + { + 'user_id': self.user.id, + 'course_id': self.course_key.to_deprecated_string(), + 'mode': "verified", + } + ) + + # Check that submission event was logged, and that it was the last call + self.mock_tracker.emit.assert_called_with( # pylint: disable=maybe-no-member + 'edx.course.enrollment.reverify.submitted', + { + 'user_id': self.user.id, + 'course_id': self.course_key.to_deprecated_string(), + 'mode': "verified", + } + ) + + self.assertTrue(self.mock_tracker.emit.call_count, 2) # pylint: disable=no-member + + self.mock_tracker.emit.reset_mock() # pylint: disable=no-member + + self.assertEquals(response.status_code, 302) + try: + verification_attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user, window=window) + self.assertIsNotNone(verification_attempt) + except ObjectDoesNotExist: + self.fail('No verification object generated') + + @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) + def test_midcourse_reverify_post_failure_expired_window(self): + window = MidcourseReverificationWindowFactory( + course_id=self.course_key, + start_date=datetime.now(pytz.UTC) - timedelta(days=100), + end_date=datetime.now(pytz.UTC) - timedelta(days=50), + ) + url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_key.to_deprecated_string()}) + response = self.client.post(url, {'face_image': ','}) + self.assertEquals(response.status_code, 302) + with self.assertRaises(ObjectDoesNotExist): + SoftwareSecurePhotoVerification.objects.get(user=self.user, window=window) + + @patch('verify_student.views.render_to_response', render_mock) + def test_midcourse_reverify_dash(self): + url = reverse('verify_student_midcourse_reverify_dash') + response = self.client.get(url) + # not enrolled in any courses + self.assertEquals(response.status_code, 200) + + enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course_key) + enrollment.update_enrollment(mode="verified", is_active=True) + MidcourseReverificationWindowFactory(course_id=self.course_key) + response = self.client.get(url) + # enrolled in a verified course, and the window is open + self.assertEquals(response.status_code, 200) + + @patch('verify_student.views.render_to_response', render_mock) + def test_midcourse_reverify_invalid_course_id(self): + # if course id is invalid return 400 + invalid_course_key = CourseLocator('edx', 'not', 'valid') + url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': unicode(invalid_course_key)}) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + +@override_settings(MODULESTORE=MODULESTORE_CONFIG) +class TestReverificationBanner(ModuleStoreTestCase): + """ + Tests for toggling the "midcourse reverification failed" banner off. + """ + + @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) + def setUp(self): + self.user = UserFactory.create(username="rusty", password="test") + self.client.login(username="rusty", password="test") + self.course_id = 'Robot/999/Test_Course' + CourseFactory.create(org='Robot', number='999', display_name=u'Test Course é') + self.window = MidcourseReverificationWindowFactory(course_id=self.course_id) + url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_id}) + self.client.post(url, {'face_image': ','}) + photo_verification = SoftwareSecurePhotoVerification.objects.get(user=self.user, window=self.window) + photo_verification.status = 'denied' + photo_verification.save() + + def test_banner_display_off(self): + self.client.post(reverse('verify_student_toggle_failed_banner_off')) + photo_verification = SoftwareSecurePhotoVerification.objects.get(user=self.user, window=self.window) + self.assertFalse(photo_verification.display) diff --git a/lms/djangoapps/verify_student/urls.py b/lms/djangoapps/verify_student/urls.py index d9db334d36..2547b66d4c 100644 --- a/lms/djangoapps/verify_student/urls.py +++ b/lms/djangoapps/verify_student/urls.py @@ -8,25 +8,76 @@ from django.conf import settings urlpatterns = patterns( '', + + # The user is starting the verification / payment process, + # most likely after enrolling in a course and selecting + # a "verified" track. url( - r'^show_requirements/{}/$'.format(settings.COURSE_ID_PATTERN), - views.show_requirements, - name="verify_student_show_requirements" + r'^start-flow/{course}/$'.format(course=settings.COURSE_ID_PATTERN), + # Pylint seems to dislike the as_view() method because as_view() is + # decorated with `classonlymethod` instead of `classmethod`. + views.PayAndVerifyView.as_view(), # pylint: disable=no-value-for-parameter + name="verify_student_start_flow", + kwargs={ + 'message': PayAndVerifyView.FIRST_TIME_VERIFY_MSG + } ), - # pylint sometimes seems to dislike the as_view() function because as_view() is - # decorated with `classonlymethod` instead of `classmethod`. It's inconsistent - # about *which* as_view() calls it grumbles about, but we disable those warnings + # The user is enrolled in a non-paid mode and wants to upgrade. + # This is the same as the "start verification" flow, + # except with slight messaging changes. url( - r'^verify/{}/$'.format(settings.COURSE_ID_PATTERN), - views.VerifyView.as_view(), # pylint: disable=no-value-for-parameter - name="verify_student_verify" + r'^upgrade/{course}/$'.format(course=settings.COURSE_ID_PATTERN), + views.PayAndVerifyView.as_view(), # pylint: disable=no-value-for-parameter + name="verify_student_upgrade_and_verify", + kwargs={ + 'message': PayAndVerifyView.UPGRADE_MSG + } ), + # The user has paid and still needs to verify. + # Since the user has "just paid", we display *all* steps + # including payment. The user resumes the flow + # from the verification step. + # Note that if the user has already verified, this will redirect + # to the dashboard. url( - r'^verified/{}/$'.format(settings.COURSE_ID_PATTERN), - views.VerifiedView.as_view(), - name="verify_student_verified" + r'^verify-now/{course}/$'.format(course=settings.COURSE_ID_PATTERN), + views.PayAndVerifyView.as_view(), # pylint: disable=no-value-for-parameter + name="verify_student_verify_now", + kwargs={ + 'always_show_payment': True, + 'current_step': PayAndVerifyView.FACE_PHOTO_STEP, + 'message': PayAndVerifyView.VERIFY_NOW_MSG + } + ), + + # The user has paid and still needs to verify, + # but the user is NOT arriving directly from the payment flow. + # This is equivalent to starting a new flow + # with the payment steps and requirements hidden + # (since the user already paid). + url( + r'^verify-later/{course}/$'.format(course=settings.COURSE_ID_PATTERN), + views.PayAndVerifyView.as_view(), # pylint: disable=no-value-for-parameter + name="verify_student_verify_later", + kwargs={ + 'message': PayAndVerifyView.VERIFY_LATER_MSG + } + ), + + # The user is returning to the flow after paying. + # This usually occurs after a redirect from the shopping cart + # once the order has been fulfilled. + url( + r'^payment-confirmation/{course}/$'.format(course=settings.COURSE_ID_PATTERN), + views.PayAndVerifyView.as_view(), # pylint: disable=no-value-for-parameter + name="verify_student_payment_confirmation", + kwargs={ + 'always_show_payment': True, + 'current_step': PayAndVerifyView.PAYMENT_CONFIRMATION_STEP, + 'message': PayAndVerifyView.PAYMENT_CONFIRMATION_MSG + } ), url( @@ -82,86 +133,10 @@ urlpatterns = patterns( views.toggle_failed_banner_off, name="verify_student_toggle_failed_banner_off" ), + + url( + r'^submit-photos/$', + views.submit_photos_for_verification, + name="verify_student_submit_photos" + ), ) - - -if settings.FEATURES.get("SEPARATE_VERIFICATION_FROM_PAYMENT"): - - urlpatterns += patterns( - '', - - url( - r'^submit-photos/$', - views.submit_photos_for_verification, - name="verify_student_submit_photos" - ), - - # The user is starting the verification / payment process, - # most likely after enrolling in a course and selecting - # a "verified" track. - url( - r'^start-flow/{course}/$'.format(course=settings.COURSE_ID_PATTERN), - views.PayAndVerifyView.as_view(), # pylint: disable=no-value-for-parameter - name="verify_student_start_flow", - kwargs={ - 'message': PayAndVerifyView.FIRST_TIME_VERIFY_MSG - } - ), - - # The user is enrolled in a non-paid mode and wants to upgrade. - # This is the same as the "start verification" flow, - # except with slight messaging changes. - url( - r'^upgrade/{course}/$'.format(course=settings.COURSE_ID_PATTERN), - views.PayAndVerifyView.as_view(), # pylint: disable=no-value-for-parameter - name="verify_student_upgrade_and_verify", - kwargs={ - 'message': PayAndVerifyView.UPGRADE_MSG - } - ), - - # The user has paid and still needs to verify. - # Since the user has "just paid", we display *all* steps - # including payment. The user resumes the flow - # from the verification step. - # Note that if the user has already verified, this will redirect - # to the dashboard. - url( - r'^verify-now/{course}/$'.format(course=settings.COURSE_ID_PATTERN), - views.PayAndVerifyView.as_view(), # pylint: disable=no-value-for-parameter - name="verify_student_verify_now", - kwargs={ - 'always_show_payment': True, - 'current_step': PayAndVerifyView.FACE_PHOTO_STEP, - 'message': PayAndVerifyView.VERIFY_NOW_MSG - } - ), - - # The user has paid and still needs to verify, - # but the user is NOT arriving directly from the payment flow. - # This is equivalent to starting a new flow - # with the payment steps and requirements hidden - # (since the user already paid). - url( - r'^verify-later/{course}/$'.format(course=settings.COURSE_ID_PATTERN), - views.PayAndVerifyView.as_view(), # pylint: disable=no-value-for-parameter - name="verify_student_verify_later", - kwargs={ - 'message': PayAndVerifyView.VERIFY_LATER_MSG - } - ), - - # The user is returning to the flow after paying. - # This usually occurs after a redirect from the shopping cart - # once the order has been fulfilled. - url( - r'^payment-confirmation/{course}/$'.format(course=settings.COURSE_ID_PATTERN), - views.PayAndVerifyView.as_view(), # pylint: disable=no-value-for-parameter - name="verify_student_payment_confirmation", - kwargs={ - 'always_show_payment': True, - 'current_step': PayAndVerifyView.PAYMENT_CONFIRMATION_STEP, - 'message': PayAndVerifyView.PAYMENT_CONFIRMATION_MSG - } - ), - ) diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 4a80fd3631..2ac8704551 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -56,126 +56,6 @@ EVENT_NAME_USER_SUBMITTED_MIDCOURSE_REVERIFY = 'edx.course.enrollment.reverify.s EVENT_NAME_USER_REVERIFICATION_REVIEWED_BY_SOFTWARESECURE = 'edx.course.enrollment.reverify.reviewed' -class VerifyView(View): - - @method_decorator(login_required) - def get(self, request, course_id): - """ - Displays the main verification view, which contains three separate steps: - - Taking the standard face photo - - Taking the id photo - - Confirming that the photos and payment price are correct - before proceeding to payment - """ - upgrade = request.GET.get('upgrade', False) - - course_id = CourseKey.from_string(course_id) - # If the user has already been verified within the given time period, - # redirect straight to the payment -- no need to verify again. - if SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user): - return redirect( - reverse('verify_student_verified', - kwargs={'course_id': course_id.to_deprecated_string()}) + "?upgrade={}".format(upgrade) - ) - elif CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == ('verified', True): - return redirect(reverse('dashboard')) - else: - # If they haven't completed a verification attempt, we have to - # restart with a new one. We can't reuse an older one because we - # won't be able to show them their encrypted photo_id -- it's easier - # bookkeeping-wise just to start over. - progress_state = "start" - - # we prefer professional over verify - current_mode = CourseMode.verified_mode_for_course(course_id) - - # if the course doesn't have a verified mode, we want to kick them - # from the flow - if not current_mode: - return redirect(reverse('dashboard')) - if course_id.to_deprecated_string() in request.session.get("donation_for_course", {}): - chosen_price = request.session["donation_for_course"][unicode(course_id)] - else: - chosen_price = current_mode.min_price - - course = modulestore().get_course(course_id) - if current_mode.suggested_prices != '': - suggested_prices = [ - decimal.Decimal(price) - for price in current_mode.suggested_prices.split(",") - ] - else: - suggested_prices = [] - - context = { - "progress_state": progress_state, - "user_full_name": request.user.profile.name, - "course_id": course_id.to_deprecated_string(), - "course_modes_choose_url": reverse('course_modes_choose', kwargs={'course_id': course_id.to_deprecated_string()}), - "course_name": course.display_name_with_default, - "course_org": course.display_org_with_default, - "course_num": course.display_number_with_default, - "purchase_endpoint": get_purchase_endpoint(), - "suggested_prices": suggested_prices, - "currency": current_mode.currency.upper(), - "chosen_price": chosen_price, - "min_price": current_mode.min_price, - "upgrade": upgrade == u'True', - "can_audit": CourseMode.mode_for_course(course_id, 'audit') is not None, - "modes_dict": CourseMode.modes_for_course_dict(course_id), - "retake": request.GET.get('retake', False), - } - - return render_to_response('verify_student/photo_verification.html', context) - - -class VerifiedView(View): - """ - View that gets shown once the user has already gone through the - verification flow - """ - @method_decorator(login_required) - def get(self, request, course_id): - """ - Handle the case where we have a get request - """ - upgrade = request.GET.get('upgrade', False) - course_id = CourseKey.from_string(course_id) - if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == ('verified', True): - return redirect(reverse('dashboard')) - - modes_dict = CourseMode.modes_for_course_dict(course_id) - - # we prefer professional over verify - current_mode = CourseMode.verified_mode_for_course(course_id) - - # if the course doesn't have a verified mode, we want to kick them - # from the flow - if not current_mode: - return redirect(reverse('dashboard')) - if course_id.to_deprecated_string() in request.session.get("donation_for_course", {}): - chosen_price = request.session["donation_for_course"][unicode(course_id)] - else: - chosen_price = current_mode.min_price - - course = modulestore().get_course(course_id) - context = { - "course_id": course_id.to_deprecated_string(), - "course_modes_choose_url": reverse('course_modes_choose', kwargs={'course_id': course_id.to_deprecated_string()}), - "course_name": course.display_name_with_default, - "course_org": course.display_org_with_default, - "course_num": course.display_number_with_default, - "purchase_endpoint": get_purchase_endpoint(), - "currency": current_mode.currency.upper(), - "chosen_price": chosen_price, - "create_order_url": reverse("verify_student_create_order"), - "upgrade": upgrade == u'True', - "can_audit": "audit" in modes_dict, - "modes_dict": modes_dict, - } - return render_to_response('verify_student/verified.html', context) - - class PayAndVerifyView(View): """View for the "verify and pay" flow. @@ -906,41 +786,6 @@ def results_callback(request): return HttpResponse("OK!") -@login_required -def show_requirements(request, course_id): - """ - Show the requirements necessary for the verification flow. - """ - # TODO: seems borked for professional; we're told we need to take photos even if there's a pending verification - course_id = CourseKey.from_string(course_id) - upgrade = request.GET.get('upgrade', False) - if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == ('verified', True): - return redirect(reverse('dashboard')) - if SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user): - return redirect( - reverse( - 'verify_student_verified', - kwargs={'course_id': course_id.to_deprecated_string()} - ) + "?upgrade={}".format(upgrade) - ) - - upgrade = request.GET.get('upgrade', False) - course = modulestore().get_course(course_id) - modes_dict = CourseMode.modes_for_course_dict(course_id) - context = { - "course_id": course_id.to_deprecated_string(), - "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_id.to_deprecated_string()}), - "verify_student_url": reverse('verify_student_verify', kwargs={'course_id': course_id.to_deprecated_string()}), - "course_name": course.display_name_with_default, - "course_org": course.display_org_with_default, - "course_num": course.display_number_with_default, - "is_not_active": not request.user.is_active, - "upgrade": upgrade == u'True', - "modes_dict": modes_dict, - } - return render_to_response("verify_student/show_requirements.html", context) - - class ReverifyView(View): """ The main reverification view. Under similar constraints as the main verification view. diff --git a/lms/envs/bok_choy.env.json b/lms/envs/bok_choy.env.json index 84b5072f92..7f60988d85 100644 --- a/lms/envs/bok_choy.env.json +++ b/lms/envs/bok_choy.env.json @@ -73,7 +73,6 @@ "ENABLE_S3_GRADE_DOWNLOADS": true, "ENABLE_THIRD_PARTY_AUTH": true, "ENABLE_COMBINED_LOGIN_REGISTRATION": true, - "SEPARATE_VERIFICATION_FROM_PAYMENT": true, "PREVIEW_LMS_BASE": "localhost:8003", "SUBDOMAIN_BRANDING": false, "SUBDOMAIN_COURSE_LISTINGS": false, diff --git a/lms/envs/common.py b/lms/envs/common.py index 28050f4876..0262935756 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -311,9 +311,6 @@ FEATURES = { # Enable display of enrollment counts in instructor and legacy analytics dashboard 'DISPLAY_ANALYTICS_ENROLLMENTS': True, - # Separate the verification flow from the payment flow - 'SEPARATE_VERIFICATION_FROM_PAYMENT': False, - # Show the mobile app links in the footer 'ENABLE_FOOTER_MOBILE_APP_LINKS': False, diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index 1b794bdc61..c1d4ccfc6a 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -30,14 +30,10 @@ from student.helpers import (
  • % if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'): % if enrollment.mode == "verified": - % if settings.FEATURES.get('SEPARATE_VERIFICATION_FROM_PAYMENT') and request.session.get('separate-verified', False): - % if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_APPROVED]: - <% mode_class = " verified" %> - % else: - <% mode_class = " honor" %> - % endif - % else: + % if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_APPROVED]: <% mode_class = " verified" %> + % else: + <% mode_class = " honor" %> % endif % else: <% mode_class = " " + enrollment.mode %> @@ -69,33 +65,25 @@ from student.helpers import ( % endif % if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'): % if enrollment.mode == "verified": - % if settings.FEATURES.get('SEPARATE_VERIFICATION_FROM_PAYMENT') and request.session.get('separate-verified', False): - % if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED]: - - ${_("Enrolled as: ")} - ## Translators: This text describes that the student has enrolled for a Verified Certificate, but verification of identity is pending. - ${_( - ## Translators: The student is enrolled for a Verified Certificate, but verification of identity is pending. -
    ${_("Verified: Pending Verification")}
    -
    - % elif verification_status.get('status') == VERIFY_STATUS_APPROVED: - - ${_("Enrolled as: ")} - ${_( -
    ${_("Verified")}
    -
    - % else: - - ${_("Enrolled as: ")} -
    ${_("Honor Code")}
    -
    - % endif - % else: + % if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED]: + + ${_("Enrolled as: ")} + ## Translators: This text describes that the student has enrolled for a Verified Certificate, but verification of identity is pending. + ${_( + ## Translators: The student is enrolled for a Verified Certificate, but verification of identity is pending. +
    ${_("Verified: Pending Verification")}
    +
    + % elif verification_status.get('status') == VERIFY_STATUS_APPROVED: ${_("Enrolled as: ")} ${_(
    ${_("Verified")}
    + % else: + + ${_("Enrolled as: ")} +
    ${_("Honor Code")}
    +
    % endif % elif enrollment.mode == "honor": @@ -146,37 +134,35 @@ from student.helpers import ( <%include file='_dashboard_certificate_information.html' args='cert_status=cert_status,course=course, enrollment=enrollment'/> % endif - % if settings.FEATURES.get('SEPARATE_VERIFICATION_FROM_PAYMENT') and request.session.get('separate-verified', True): - % if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_APPROVED] and not is_course_blocked: -
    - % if verification_status['status'] == VERIFY_STATUS_NEED_TO_VERIFY: -
    - % if verification_status['days_until_deadline'] is not None: -

    ${_('Verification not yet complete.')}

    -

    ${ungettext( - 'You only have {days} day left to verify for this course.', - 'You only have {days} days left to verify for this course.', - verification_status['days_until_deadline'] - ).format(days=verification_status['days_until_deadline'])}

    - % else: -

    ${_('Almost there!')}

    -

    ${_('You still need to verify for this course.')}

    - % endif -
    - - % elif verification_status['status'] == VERIFY_STATUS_SUBMITTED: -

    ${_('You have already verified your ID!')}

    -

    ${_('Thanks for your patience as we process your request.')}

    - % elif verification_status['status'] == VERIFY_STATUS_APPROVED: -

    ${_('You have already verified your ID!')}

    - % if verification_status['verification_good_until'] is not None: -

    ${_('Your verification status is good until {date}.').format(date=verification_status['verification_good_until'])} + % if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_APPROVED] and not is_course_blocked: +

    + % if verification_status['status'] == VERIFY_STATUS_NEED_TO_VERIFY: +
    + % if verification_status['days_until_deadline'] is not None: +

    ${_('Verification not yet complete.')}

    +

    ${ungettext( + 'You only have {days} day left to verify for this course.', + 'You only have {days} days left to verify for this course.', + verification_status['days_until_deadline'] + ).format(days=verification_status['days_until_deadline'])}

    + % else: +

    ${_('Almost there!')}

    +

    ${_('You still need to verify for this course.')}

    % endif +
    + + % elif verification_status['status'] == VERIFY_STATUS_SUBMITTED: +

    ${_('You have already verified your ID!')}

    +

    ${_('Thanks for your patience as we process your request.')}

    + % elif verification_status['status'] == VERIFY_STATUS_APPROVED: +

    ${_('You have already verified your ID!')}

    + % if verification_status['verification_good_until'] is not None: +

    ${_('Your verification status is good until {date}.').format(date=verification_status['verification_good_until'])} % endif -

    % endif +
    % endif % if course_mode_info['show_upsell'] and not is_course_blocked: @@ -195,11 +181,7 @@ from student.helpers import (
    • - % if settings.FEATURES.get('SEPARATE_VERIFICATION_FROM_PAYMENT') and request.session.get('separate-verified', False): - % else: - - % endif ${_( ${_("Upgrade to Verified Track")} diff --git a/lms/templates/emails/order_confirmation_email.txt b/lms/templates/emails/order_confirmation_email.txt index 863b1de956..706378158d 100644 --- a/lms/templates/emails/order_confirmation_email.txt +++ b/lms/templates/emails/order_confirmation_email.txt @@ -39,5 +39,5 @@ ${order.bill_to_country.upper()} % endif % for order_item in order_items: -${order_item.additional_instruction_text(separate_verification=getattr(request, 'session', {}).get('separate-verified', False))} +${order_item.additional_instruction_text()} % endfor diff --git a/lms/templates/verify_student/final_verification.html b/lms/templates/verify_student/final_verification.html deleted file mode 100644 index c9abf876ae..0000000000 --- a/lms/templates/verify_student/final_verification.html +++ /dev/null @@ -1,10 +0,0 @@ -<%! from django.utils.translation import ugettext as _ %> -<%! from django.core.urlresolvers import reverse %> -<%inherit file="../main.html" /> - -<%block name="content"> - -Final Verification! - - - diff --git a/lms/templates/verify_student/photo_verification.html b/lms/templates/verify_student/photo_verification.html deleted file mode 100644 index 78ffc31f88..0000000000 --- a/lms/templates/verify_student/photo_verification.html +++ /dev/null @@ -1,461 +0,0 @@ -<%! from django.utils.translation import ugettext as _ %> -<%! from django.core.urlresolvers import reverse %> -<%! from lms.envs.common import TECH_SUPPORT_EMAIL %> -<%inherit file="../main.html" /> -<%namespace name='static' file='/static_content.html'/> - -<%block name="bodyclass">register verification-process step-photos ${'is-upgrading' if upgrade else ''} -<%block name="pagetitle"> - %if upgrade: - ${_("Upgrade Your Registration for {} | Verification").format(course_name)} - %else: - ${_("Register for {} | Verification").format(course_name)} - %endif - - -<%block name="js_extra"> - - - - - -<%block name="content"> - - - - - - - - - -
      -
      - - <%include file="_verification_header.html" args="course_name=course_name" /> - -
      -
      -

      ${_("Your Progress")}

      - -
        -
      1. - 0 - ${_("Intro")} -
      2. - -
      3. - 1 - ${_("Current Step: ")}${_("Take Photo")} -
      4. - -
      5. - 2 - ${_("Take ID Photo")} -
      6. - -
      7. - 3 - ${_("Review")} -
      8. - -
      9. - 4 - ${_("Make Payment")} -
      10. - -
      11. - - - - ${_("Confirmation")} -
      12. -
      - - - - -
      -
      - -
      -
      - - -
      -
      - - <%include file="_verification_support.html" /> -
      -
      - -<%include file="_modal_editname.html" /> - diff --git a/lms/templates/verify_student/show_requirements.html b/lms/templates/verify_student/show_requirements.html deleted file mode 100644 index e4e8af93a0..0000000000 --- a/lms/templates/verify_student/show_requirements.html +++ /dev/null @@ -1,187 +0,0 @@ -<%! from django.utils.translation import ugettext as _ %> -<%! from django.core.urlresolvers import reverse %> - -<%inherit file="../main.html" /> -<%block name="bodyclass">register verification-process step-requirements ${'is-upgrading' if upgrade else ''} -<%block name="pagetitle"> - %if upgrade: - ${_("Upgrade Your Registration for {}").format(course_name)} - %else: - ${_("Register for {}").format(course_name)} - %endif - - -<%block name="content"> -%if is_not_active: -
      -
      - -
      -

      ${_("You need to activate your {platform_name} account before proceeding").format(platform_name=settings.PLATFORM_NAME)}

      -
      -

      ${_("Please check your email for further instructions on activating your new account.")}

      -
      -
      -
      -
      -%endif - -
      -
      - - <%include file="_verification_header.html" args="course_name=course_name"/> - -
      -
      -

      ${_("Your Progress")}

      - -
        -
      1. - 0 - ${_("Current Step: ")}${_("Intro")} -
      2. - -
      3. - 1 - ${_("Take Photo")} -
      4. - -
      5. - 2 - ${_("Take ID Photo")} -
      6. - -
      7. - 3 - ${_("Review")} -
      8. - -
      9. - 4 - ${_("Make Payment")} -
      10. - -
      11. - - - - ${_("Confirmation")} -
      12. -
      - - - - -
      -
      - - -
      -
      - %if upgrade: -

      ${_("What You Will Need to Upgrade")}

      - -
      -

      ${_("There are three things you will need to upgrade to being an ID verified student:")}

      -
      - %else: -

      ${_("What You Will Need to Register")}

      - -
      -

      ${_("There are three things you will need to register as an ID verified student:")}

      -
      - %endif - -
        - %if is_not_active: -
      • -

        ${_("Activate Your Account")}

        -
        - -
        - -
        -

        - ${_("Check your email")} - ${_("You need to activate your {platform_name} account before you can register for courses. Check your inbox for an activation email.").format(platform_name=settings.PLATFORM_NAME)} -

        -
        -
      • - %endif - -
      • -

        ${_("Identification")}

        -
        - - - - -
        - -
        -

        - ${_("A photo identification document")} - ${_("A driver's license, passport, or other government or school-issued ID with your name and picture on it.")} -

        -
        -
      • - -
      • -

        ${_("Webcam")}

        -
        - -
        - -
        -

        - ${_("A webcam and a modern browser")} - - Firefox, - Chrome, - Safari, - ## Translators: This phrase will look like this: "Internet Explorer 9 or later" - ${_("{internet_explorer_version} or later").format(internet_explorer_version="Internet Explorer 9")}. - ${_("Please make sure your browser is updated to the most recent version possible.")} - -

        -
        -
      • - -
      • -

        ${_("Credit or Debit Card")}

        -
        - -
        - -
        -

        - ${_("A major credit or debit card")} - ${_("Visa, MasterCard, American Express, Discover, Diners Club, or JCB with the Discover logo.")} -

        -
        -
      • -
      - - -
      -
      - - <%include file="_verification_support.html" /> -
      -
      - diff --git a/lms/templates/verify_student/verified.html b/lms/templates/verify_student/verified.html deleted file mode 100644 index 54b9b3873e..0000000000 --- a/lms/templates/verify_student/verified.html +++ /dev/null @@ -1,114 +0,0 @@ -<%! from django.utils.translation import ugettext as _ %> -<%! from django.core.urlresolvers import reverse %> -<%! from django.template.defaultfilters import escapejs %> - -<%inherit file="../main.html" /> -<%namespace name='static' file='/static_content.html'/> - -<%block name="bodyclass">register verification-process is-verified -<%block name="pagetitle">${_("Register for {} | Verification").format(course_name)} - -<%block name="js_extra"> - - - -<%block name="content"> -
      -
      - - <%include file="_verification_header.html" /> - -
      -
      -

      ${_("Your Progress")}

      - -
        -
      1. - 1 - ${_("ID Verification")} -
      2. -
      3. - 2 - ${_("Current Step: ")}${_("Review")} -
      4. - -
      5. - 3 - ${_("Make Payment")} -
      6. - -
      7. - - - - ${_("Confirmation")} -
      8. -
      - - - - -
      -
      - -
      -
      -

      ${_("You've Been Verified Previously")}

      - -
      -

      ${_("We've already verified your identity (through the photos of you and your ID you provided earlier). You can proceed to make your secure payment and complete registration.")}

      -
      - - -
      -
      - - <%include file="_verification_support.html" /> -
      -
      -