Commit 80589eab by Renzo Lucioni

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.
parent 1ad0e9fd
......@@ -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")
......
......@@ -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
......
......@@ -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')
......
......@@ -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(
......
......@@ -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)
......
......@@ -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
......
......@@ -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):
......
......@@ -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()
......
......@@ -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
......
@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 "<PhotoType>" photo
# When I retake my "<PhotoType>" 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
# 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)
......@@ -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)
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
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)
return "{verification_reminder} {refund_reminder}".format(
verification_reminder=verification_reminder,
refund_reminder=refund_reminder
)
@classmethod
def verified_certificates_count(cls, course_id, status):
......
......@@ -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
......
......@@ -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})
......
......@@ -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)}
)
# 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)
# 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)
return render_to_response(receipt_template, context)
......
......@@ -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)
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.
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'])
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')
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -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'^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
}
),
# 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'^show_requirements/{}/$'.format(settings.COURSE_ID_PATTERN),
views.show_requirements,
name="verify_student_show_requirements"
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
}
),
# 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 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/{}/$'.format(settings.COURSE_ID_PATTERN),
views.VerifyView.as_view(), # pylint: disable=no-value-for-parameter
name="verify_student_verify"
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'^verified/{}/$'.format(settings.COURSE_ID_PATTERN),
views.VerifiedView.as_view(),
name="verify_student_verified"
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"
),
)
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
}
),
)
url(
r'^submit-photos/$',
views.submit_photos_for_verification,
name="verify_student_submit_photos"
),
)
......@@ -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.
......
......@@ -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,
......
......@@ -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,
......
......@@ -30,14 +30,10 @@ from student.helpers import (
<li class="course-item">
% 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]:
<span class="sts-enrollment" title="${_("Your verification is pending")}">
<span class="label">${_("Enrolled as: ")}</span>
## Translators: This text describes that the student has enrolled for a Verified Certificate, but verification of identity is pending.
<img class="deco-graphic" src="${static.url('images/verified-ribbon.png')}" alt="${_("ID verification pending")}" />
## Translators: The student is enrolled for a Verified Certificate, but verification of identity is pending.
<div class="sts-enrollment-value">${_("Verified: Pending Verification")}</div>
</span>
% elif verification_status.get('status') == VERIFY_STATUS_APPROVED:
<span class="sts-enrollment" title="${_("You're enrolled as a verified student")}">
<span class="label">${_("Enrolled as: ")}</span>
<img class="deco-graphic" src="${static.url('images/verified-ribbon.png')}" alt="${_("ID Verified Ribbon/Badge")}" />
<div class="sts-enrollment-value">${_("Verified")}</div>
</span>
% else:
<span class="sts-enrollment" title="${_("You're enrolled as an honor code student")}">
<span class="label">${_("Enrolled as: ")}</span>
<div class="sts-enrollment-value">${_("Honor Code")}</div>
</span>
% endif
% else:
% if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED]:
<span class="sts-enrollment" title="${_("Your verification is pending")}">
<span class="label">${_("Enrolled as: ")}</span>
## Translators: This text describes that the student has enrolled for a Verified Certificate, but verification of identity is pending.
<img class="deco-graphic" src="${static.url('images/verified-ribbon.png')}" alt="${_("ID verification pending")}" />
## Translators: The student is enrolled for a Verified Certificate, but verification of identity is pending.
<div class="sts-enrollment-value">${_("Verified: Pending Verification")}</div>
</span>
% elif verification_status.get('status') == VERIFY_STATUS_APPROVED:
<span class="sts-enrollment" title="${_("You're enrolled as a verified student")}">
<span class="label">${_("Enrolled as: ")}</span>
<img class="deco-graphic" src="${static.url('images/verified-ribbon.png')}" alt="${_("ID Verified Ribbon/Badge")}" />
<div class="sts-enrollment-value">${_("Verified")}</div>
</span>
% else:
<span class="sts-enrollment" title="${_("You're enrolled as an honor code student")}">
<span class="label">${_("Enrolled as: ")}</span>
<div class="sts-enrollment-value">${_("Honor Code")}</div>
</span>
% endif
% elif enrollment.mode == "honor":
<span class="sts-enrollment" title="${_("You're enrolled as an honor code student")}">
......@@ -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:
<div class="message message-status is-shown">
% if verification_status['status'] == VERIFY_STATUS_NEED_TO_VERIFY:
<div class="verification-reminder">
% if verification_status['days_until_deadline'] is not None:
<h4 class="message-title">${_('Verification not yet complete.')}</h4>
<p class="message-copy">${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'])}</p>
% else:
<h4 class="message-title">${_('Almost there!')}</h4>
<p class="message-copy">${_('You still need to verify for this course.')}</p>
% endif
</div>
<div class="verification-cta">
<a href="${reverse('verify_student_verify_later', kwargs={'course_id': unicode(course.id)})}" class="cta" data-course-id="${course.id | h}">${_('Verify Now')}</a>
</div>
% elif verification_status['status'] == VERIFY_STATUS_SUBMITTED:
<h4 class="message-title">${_('You have already verified your ID!')}</h4>
<p class="message-copy">${_('Thanks for your patience as we process your request.')}</p>
% elif verification_status['status'] == VERIFY_STATUS_APPROVED:
<h4 class="message-title">${_('You have already verified your ID!')}</h4>
% if verification_status['verification_good_until'] is not None:
<p class="message-copy">${_('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:
<div class="message message-status is-shown">
% if verification_status['status'] == VERIFY_STATUS_NEED_TO_VERIFY:
<div class="verification-reminder">
% if verification_status['days_until_deadline'] is not None:
<h4 class="message-title">${_('Verification not yet complete.')}</h4>
<p class="message-copy">${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'])}</p>
% else:
<h4 class="message-title">${_('Almost there!')}</h4>
<p class="message-copy">${_('You still need to verify for this course.')}</p>
% endif
</div>
<div class="verification-cta">
<a href="${reverse('verify_student_verify_later', kwargs={'course_id': unicode(course.id)})}" class="cta" data-course-id="${course.id | h}">${_('Verify Now')}</a>
</div>
% elif verification_status['status'] == VERIFY_STATUS_SUBMITTED:
<h4 class="message-title">${_('You have already verified your ID!')}</h4>
<p class="message-copy">${_('Thanks for your patience as we process your request.')}</p>
% elif verification_status['status'] == VERIFY_STATUS_APPROVED:
<h4 class="message-title">${_('You have already verified your ID!')}</h4>
% if verification_status['verification_good_until'] is not None:
<p class="message-copy">${_('Your verification status is good until {date}.').format(date=verification_status['verification_good_until'])}
% endif
</div>
% endif
</div>
% endif
% if course_mode_info['show_upsell'] and not is_course_blocked:
......@@ -195,11 +181,7 @@ from student.helpers import (
<ul class="actions message-actions">
<li class="action-item">
% if settings.FEATURES.get('SEPARATE_VERIFICATION_FROM_PAYMENT') and request.session.get('separate-verified', False):
<a class="action action-upgrade" href="${reverse('verify_student_upgrade_and_verify', kwargs={'course_id': unicode(course.id)})}" data-course-id="${course.id | h}" data-user="${user.username | h}">
% else:
<a class="action action-upgrade" href="${reverse('course_modes_choose', kwargs={'course_id': unicode(course.id)})}?upgrade=True" data-course-id="${course.id | h}" data-user="${user.username | h}">
% endif
<img class="deco-graphic" src="${static.url('images/vcert-ribbon-s.png')}" alt="${_("ID Verified Ribbon/Badge")}">
<span class="wrapper-copy">
<span class="copy" id="upgrade-to-verified">${_("Upgrade to Verified Track")}</span>
......
......@@ -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
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%inherit file="../main.html" />
<%block name="content">
Final Verification!
</%block>
<%! 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>
<%block name="pagetitle">
%if upgrade:
${_("Upgrade Your Registration for {}").format(course_name)}
%else:
${_("Register for {}").format(course_name)}
%endif
</%block>
<%block name="content">
%if is_not_active:
<div class="wrapper-msg wrapper-msg-activate">
<div class=" msg msg-activate">
<i class="msg-icon fa fa-warning"></i>
<div class="msg-content">
<h3 class="title">${_("You need to activate your {platform_name} account before proceeding").format(platform_name=settings.PLATFORM_NAME)}</h3>
<div class="copy">
<p>${_("Please check your email for further instructions on activating your new account.")}</p>
</div>
</div>
</div>
</div>
%endif
<div class="container">
<section class="wrapper">
<%include file="_verification_header.html" args="course_name=course_name"/>
<div class="wrapper-progress">
<section class="progress">
<h3 class="sr title">${_("Your Progress")}</h3>
<ol class="progress-steps">
<li class="progress-step is-current" id="progress-step0">
<span class="wrapper-step-number"><span class="step-number">0</span></span>
<span class="step-name"><span class="sr">${_("Current Step: ")}</span>${_("Intro")}</span>
</li>
<li class="progress-step" id="progress-step1">
<span class="wrapper-step-number"><span class="step-number">1</span></span>
<span class="step-name">${_("Take Photo")}</span>
</li>
<li class="progress-step" id="progress-step2">
<span class="wrapper-step-number"><span class="step-number">2</span></span>
<span class="step-name">${_("Take ID Photo")}</span>
</li>
<li class="progress-step" id="progress-step3">
<span class="wrapper-step-number"><span class="step-number">3</span></span>
<span class="step-name">${_("Review")}</span>
</li>
<li class="progress-step" id="progress-step4">
<span class="wrapper-step-number"><span class="step-number">4</span></span>
<span class="step-name">${_("Make Payment")}</span>
</li>
<li class="progress-step progress-step-icon" id="progress-step5">
<span class="wrapper-step-number"><span class="step-number">
<i class="icon fa fa-check-square-o"></i>
</span></span>
<span class="step-name">${_("Confirmation")}</span>
</li>
</ol>
<span class="progress-sts">
<span class="progress-sts-value"></span>
</span>
</section>
</div>
<div class="wrapper-content-main">
<article class="content-main">
%if upgrade:
<h3 class="title">${_("What You Will Need to Upgrade")}</h3>
<div class="instruction">
<p>${_("There are three things you will need to upgrade to being an ID verified student:")}</p>
</div>
%else:
<h3 class="title">${_("What You Will Need to Register")}</h3>
<div class="instruction">
<p>${_("There are three things you will need to register as an ID verified student:")}</p>
</div>
%endif
<ul class="list-reqs ${"account-not-activated" if is_not_active else ""}">
%if is_not_active:
<li class="req req-0 req-activate">
<h4 class="title">${_("Activate Your Account")}</h4>
<div class="placeholder-art">
<i class="icon fa fa-envelope-o"></i>
</div>
<div class="copy">
<p>
<span class="copy-super">${_("Check your email")}</span>
<span class="copy-sub">${_("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)}</span>
</p>
</div>
</li>
%endif
<li class="req req-1 req-id">
<h4 class="title">${_("Identification")}</h4>
<div class="placeholder-art old-id-card">
<span class="fa-stack">
<i class="icon fa fa-list-alt fa-stack-2x"></i>
<i class="icon fa fa-user fa-stack-1x id-photo"></i>
</span>
</div>
<div class="copy">
<p>
<span class="copy-super">${_("A photo identification document")}</span>
<span class="copy-sub">${_("A driver's license, passport, or other government or school-issued ID with your name and picture on it.")}</span>
</p>
</div>
</li>
<li class="req req-2 req-webcam">
<h4 class="title">${_("Webcam")}</h4>
<div class="placeholder-art">
<i class="icon fa fa-video-camera"></i>
</div>
<div class="copy">
<p>
<span class="copy-super">${_("A webcam and a modern browser")}</span>
<span class="copy-sub"><strong>
<a rel="external" href="https://www.mozilla.org/en-US/firefox/new/">Firefox</a>,
<a rel="external" href="https://www.google.com/intl/en/chrome/browser/">Chrome</a>,
<a rel="external" href="http://www.apple.com/safari/">Safari</a>,
## Translators: This phrase will look like this: "Internet Explorer 9 or later"
<a rel="external" href="http://windows.microsoft.com/en-us/internet-explorer/download-ie">${_("{internet_explorer_version} or later").format(internet_explorer_version="Internet Explorer 9")}</a></strong>.
${_("Please make sure your browser is updated to the most recent version possible.")}
</span>
</p>
</div>
</li>
<li class="req req-3 req-payment">
<h4 class="title">${_("Credit or Debit Card")}</h4>
<div class="placeholder-art">
<i class="icon fa fa-credit-card"></i>
</div>
<div class="copy">
<p>
<span class="copy-super">${_("A major credit or debit card")}</span>
<span class="copy-sub">${_("Visa, MasterCard, American Express, Discover, Diners Club, or JCB with the Discover logo.")}</span>
</p>
</div>
</li>
</ul>
<nav class="nav-wizard ${"is-not-ready" if is_not_active else "is-ready"}">
%if can_audit:
%if upgrade:
<span class="help help-inline">${_("Missing something? You can always continue to audit this course instead.")}</span>
%else:
<span class="help help-inline">${_("Missing something? You can always {a_start}audit this course instead{a_end}").format(a_start='<a href="{}">'.format(course_modes_choose_url), a_end="</a>")}</span>
%endif
%endif
<ol class="wizard-steps">
<li class="wizard-step">
<a class="next action-primary ${"disabled" if is_not_active else ""}" id="face_next_button" href="${verify_student_url}?upgrade=${upgrade}">${_("Go to Step 1: Take my Photo")}</a>
</li>
</ol>
</nav>
</article>
</div> <!-- /wrapper-content-main -->
<%include file="_verification_support.html" />
</section>
</div>
</%block>
<%! 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>
<%block name="pagetitle">${_("Register for {} | Verification").format(course_name)}</%block>
<%block name="js_extra">
<script type="text/javascript">
var submitToPaymentProcessing = function(event) {
event.preventDefault();
$("#pay_button").addClass("is-disabled").attr('aria-disabled', true);
var xhr = $.post(
"${create_order_url}",
{
"course_id" : "${course_id | escapejs}",
},
function(data) {
for (prop in data) {
$('<input>').attr({
type: 'hidden',
name: prop,
value: data[prop]
}).appendTo('#pay_form');
}
}
)
.done(function(data) {
$("#pay_form").submit();
})
.fail(function(jqXhr,text_status, error_thrown) {
$("#pay_button").removeClass("is-disabled").attr('aria-disabled', false);
alert(jqXhr.responseText); });
}
$(document).ready(function() {
$("#pay_button").click(submitToPaymentProcessing);
});
</script>
</%block>
<%block name="content">
<div class="container">
<section class="wrapper">
<%include file="_verification_header.html" />
<div class="wrapper-progress">
<section class="progress">
<h3 class="sr title">${_("Your Progress")}</h3>
<ol class="progress-steps">
<li class="progress-step is-completed" id="progress-step1">
<span class="wrapper-step-number"><span class="step-number">1</span></span>
<span class="step-name">${_("ID Verification")}</span>
</li>
<li class="progress-step is-current" id="progress-step2">
<span class="wrapper-step-number"><span class="step-number">2</span></span>
<span class="step-name"><span class="sr">${_("Current Step: ")}</span>${_("Review")}</span>
</li>
<li class="progress-step" id="progress-step3">
<span class="wrapper-step-number"><span class="step-number">3</span></span>
<span class="step-name">${_("Make Payment")}</span>
</li>
<li class="progress-step progress-step-icon" id="progress-step4">
<span class="wrapper-step-number"><span class="step-number">
<i class="icon fa fa-check-square-o"></i>
</span></span>
<span class="step-name">${_("Confirmation")}</span>
</li>
</ol>
<span class="progress-sts">
<span class="progress-sts-value"></span>
</span>
</section>
</div>
<div class="wrapper-content-main">
<article class="content-main">
<h3 class="title">${_("You've Been Verified Previously")}</h3>
<div class="instruction">
<p>${_("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.")}</p>
</div>
<nav class="nav-wizard is-ready">
%if "professional" in modes_dict:
<span class="help help-inline price-value">${_("Your Course Total is $ ")} <strong>${chosen_price}</strong></span>
%else:
<span class="help help-inline price-value">${_("You have decided to pay $ ")} <strong>${chosen_price}</strong></span>
%endif
<ol class="wizard-steps">
<li class="wizard-step step-proceed">
<form id="pay_form" method="post" action="${purchase_endpoint}">
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
<input type="hidden" name="course_id" value="${course_id | h}" />
<button type="submit" class="action-primary" id="pay_button">Go to Secure Payment</button>
</form>
</li>
</ol>
</nav>
</article>
</div> <!-- /wrapper-content-main -->
<%include file="_verification_support.html" />
</section>
</div>
</%block>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment