Commit b22b0a35 by Will Daly

Merge pull request #6924 from edx/will/ECOM-1045

ECOM-1045: Add messaging for when students miss the verification deadline
parents a17a5374 0da4b13f
......@@ -1576,30 +1576,6 @@ class CertificateItem(OrderItem):
self.course_enrollment.change_mode(self.mode)
self.course_enrollment.activate()
@property
def single_item_receipt_template(self):
if self.mode in ('verified', 'professional'):
return 'shoppingcart/verified_cert_receipt.html'
else:
return super(CertificateItem, self).single_item_receipt_template
@property
def single_item_receipt_context(self):
course = modulestore().get_course(self.course_id)
return {
"course_id": self.course_id,
"course_name": course.display_name_with_default,
"course_org": course.display_org_with_default,
"course_num": course.display_number_with_default,
"course_start_date_text": course.start_datetime_text(),
"course_has_started": course.start > datetime.today().replace(tzinfo=pytz.utc),
"course_root_url": reverse(
'course_root',
kwargs={'course_id': self.course_id.to_deprecated_string()} # pylint: disable=no-member
),
"dashboard_url": reverse('dashboard'),
}
def additional_instruction_text(self):
refund_reminder = _(
"You have up to two weeks into the course to unenroll from the Verified Certificate option "
......
......@@ -610,13 +610,10 @@ class CertificateItemTest(ModuleStoreTestCase):
def test_single_item_template(self):
cart = Order.get_cart_for_user(user=self.user)
cert_item = CertificateItem.add_to_order(cart, self.course_key, self.cost, 'verified')
self.assertEquals(cert_item.single_item_receipt_template,
'shoppingcart/verified_cert_receipt.html')
self.assertEquals(cert_item.single_item_receipt_template, 'shoppingcart/receipt.html')
cert_item = CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor')
self.assertEquals(cert_item.single_item_receipt_template,
'shoppingcart/receipt.html')
self.assertEquals(cert_item.single_item_receipt_template, 'shoppingcart/receipt.html')
@override_settings(
SEGMENT_IO_LMS_KEY="foobar",
......
......@@ -1300,7 +1300,7 @@ class ReceiptRedirectTest(ModuleStoreTestCase):
password=self.PASSWORD
)
def test_show_receipt_redirect_to_verify_student(self):
def test_postpay_callback_redirect_to_verify_student(self):
# Create other carts first
# This ensures that the order ID and order item IDs do not match
Order.get_cart_for_user(self.user).start_purchase()
......@@ -1315,11 +1315,13 @@ class ReceiptRedirectTest(ModuleStoreTestCase):
self.COST,
'verified'
)
self.cart.purchase()
self.cart.start_purchase()
# Visit the receipt page
url = reverse('shoppingcart.views.show_receipt', args=[self.cart.id])
resp = self.client.get(url)
# Simulate hitting the post-pay callback
with patch('shoppingcart.views.process_postpay_callback') as mock_process:
mock_process.return_value = {'success': True, 'order': self.cart}
url = reverse('shoppingcart.views.postpay_callback')
resp = self.client.post(url, follow=True)
# Expect to be redirected to the payment confirmation
# page in the verify_student app
......@@ -1330,8 +1332,7 @@ class ReceiptRedirectTest(ModuleStoreTestCase):
redirect_url += '?payment-order-num={order_num}'.format(
order_num=self.cart.id
)
self.assertRedirects(resp, redirect_url)
self.assertIn(redirect_url, resp.redirect_chain[0][0])
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
......
......@@ -37,8 +37,9 @@ from .exceptions import (
from .models import (
Order, OrderTypes,
PaidCourseRegistration, OrderItem, Coupon,
CouponRedemption, CourseRegistrationCode, RegistrationCodeRedemption,
CourseRegCodeItem, Donation, DonationConfiguration
CertificateItem, CouponRedemption, CourseRegistrationCode,
RegistrationCodeRedemption, CourseRegCodeItem,
Donation, DonationConfiguration
)
from .processors import (
process_postpay_callback, render_purchase_form_html,
......@@ -612,6 +613,43 @@ def donate(request):
return HttpResponse(response_params, content_type="text/json")
def _get_verify_flow_redirect(order):
"""Check if we're in the verification flow and redirect if necessary.
Arguments:
order (Order): The order received by the post-pay callback.
Returns:
HttpResponseRedirect or None
"""
# See if the order contained any certificate items
# If so, the user is coming from the payment/verification flow.
cert_items = CertificateItem.objects.filter(order=order)
if cert_items.count() > 0:
# Currently, we allow the purchase of only one verified
# enrollment at a time; if there are more than one,
# this will choose the first.
if cert_items.count() > 1:
log.warning(
u"More than one certificate item in order %s; "
u"continuing with the payment/verification flow for "
u"the first order item (course %s).",
order.id, cert_items[0].course_id
)
course_id = cert_items[0].course_id
url = reverse(
'verify_student_payment_confirmation',
kwargs={'course_id': unicode(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.id)
return HttpResponseRedirect(url)
@csrf_exempt
@require_POST
def postpay_callback(request):
......@@ -626,7 +664,16 @@ def postpay_callback(request):
"""
params = request.POST.dict()
result = process_postpay_callback(params)
if result['success']:
# See if this payment occurred as part of the verification flow process
# If so, send the user back into the flow so they have the option
# to continue with verification.
verify_flow_redirect = _get_verify_flow_redirect(result['order'])
if verify_flow_redirect is not None:
return verify_flow_redirect
# Otherwise, send the user to the receipt page
return HttpResponseRedirect(reverse('shoppingcart.views.show_receipt', args=[result['order'].id]))
else:
return render_to_response('shoppingcart/error.html', {'order': result['order'],
......@@ -869,29 +916,13 @@ 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.
if order_items.count() == 1:
receipt_template = order_items[0].single_item_receipt_template
context.update(order_items[0].single_item_receipt_context)
# 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)
......
......@@ -603,6 +603,25 @@ class TestPayAndVerifyView(ModuleStoreTestCase):
data = self._get_page_data(response)
self.assertEqual(data['verification_deadline'], "Jan 02, 2999 at 00:00 UTC")
def test_course_mode_expired(self):
course = self._create_course("verified")
mode = CourseMode.objects.get(
course_id=course.id,
mode_slug="verified"
)
expiration = datetime(1999, 1, 2, tzinfo=pytz.UTC)
mode.expiration_datetime = expiration
mode.save()
# Need to be enrolled
self._enroll(course.id, "verified")
# The course mode has expired, so expect an explanation
# to the student that the deadline has passed
response = self._get_page("verify_student_verify_later", course.id)
self.assertContains(response, "verification deadline")
self.assertContains(response, "Jan 02, 1999 at 00:00 UTC")
def _create_course(self, *course_modes, **kwargs):
"""Create a new course with the specified course modes. """
course = CourseFactory.create()
......
......@@ -256,20 +256,36 @@ class PayAndVerifyView(View):
log.warn(u"No course specified for verification flow request.")
raise Http404
# Verify that the course has a verified mode
course_mode = CourseMode.verified_mode_for_course(course_key)
if course_mode is None:
# Check that the course has an unexpired verified mode
course_mode, expired_course_mode = self._get_verified_modes_for_course(course_key)
if course_mode is not None:
log.info(
u"Entering verified workflow for user '%s', course '%s', with current step '%s'.",
request.user.id, course_id, current_step
)
elif expired_course_mode is not None:
# Check if there is an *expired* verified course mode;
# if so, we should show a message explaining that the verification
# deadline has passed.
log.info(u"Verification deadline for '%s' has passed.", course_id)
context = {
'course': course,
'deadline': (
get_default_time_display(expired_course_mode.expiration_datetime)
if expired_course_mode.expiration_datetime else ""
)
}
return render_to_response("verify_student/missed_verification_deadline.html", context)
else:
# Otherwise, there has never been a verified mode,
# so return a page not found response.
log.warn(
u"No verified course mode found for course '{course_id}' for verification flow request"
.format(course_id=course_id)
u"No verified course mode found for course '%s' for verification flow request",
course_id
)
raise Http404
log.info(
u"Entering verified workflow for user '{user}', course '{course_id}', with current step '{current_step}'."
.format(user=request.user, course_id=course_id, current_step=current_step)
)
# Check whether the user has verified, paid, and enrolled.
# A user is considered "paid" if he or she has an enrollment
# with a paid course mode (such as "verified").
......@@ -427,6 +443,31 @@ class PayAndVerifyView(View):
if url is not None:
return redirect(url)
def _get_verified_modes_for_course(self, course_key):
"""Retrieve unexpired and expired verified modes for a course.
Arguments:
course_key (CourseKey): The location of the course.
Returns:
Tuple of `(verified_mode, expired_verified_mode)`. If provided,
`verified_mode` is an *unexpired* verified mode for the course.
If provided, `expired_verified_mode` is an *expired* verified
mode for the course. Either of these may be None.
"""
# Retrieve all the modes at once to reduce the number of database queries
all_modes, unexpired_modes = CourseMode.all_and_unexpired_modes_for_courses([course_key])
# Find an unexpired verified mode
verified_mode = CourseMode.verified_mode_for_course(course_key, modes=unexpired_modes[course_key])
expired_verified_mode = None
if verified_mode is None:
expired_verified_mode = CourseMode.verified_mode_for_course(course_key, modes=all_modes[course_key])
return (verified_mode, expired_verified_mode)
def _display_steps(self, always_show_payment, already_verified, already_paid):
"""Determine which steps to display to the user.
......
<%! from django.utils.translation import ugettext as _ %>
<%namespace name='static' file='../static_content.html'/>
<%inherit file="../main.html" />
<%block name="pagetitle">${_("Verification Deadline Has Passed")}</%block>
<%block name="content">
<section class="outside-app">
<p>${_(
u"The verification deadline for {course_name} was {date}. "
u"Verification is no longer available."
).format(
course_name=course.display_name,
date=deadline
)}</p>
</section>
</%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