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 _ %>
<%inherit file="../main.html" />
<%block name="bodyclass">register verification-process step-confirmation</%block>
<%block name="pagetitle">${_("Receipt (Order")} ${order.id})</%block>
<%block name="content">
% if notification is not UNDEFINED:
<section class="notification">
${notification}
</section>
% endif
<div class="container">
<section class="wrapper">
<header class="page-header">
<h2 class="title">
<span class="wrapper-sts">
<span class="sts-label">${_("You are now registered for: ")}</span>
<span class="sts-course-org">${course_org}</span>
<span class="sts-course-number">${course_num}</span>
<span class="sts-course-name">${course_name}</span>
</span>
<span class="sts-track">
<span class="sts-track-value">
<span class="context">${_("Registered as: ")}</span> ${_("ID Verified")}
</span>
</span>
</h2>
</header>
<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"><span class="sr">${_("Current Step: ")}</span>${_("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">${_("Congratulations! You are now verified on ")} ${_(settings.PLATFORM_NAME)}.</h3>
<div class="instruction">
<p>${_("You are now registered as a verified student! Your registration details are below.")}</p>
</div>
<ul class="list-info">
<li class="info-item course-info">
<h4 class="title">${_("You are registered for:")}</h4>
<div class="wrapper-report">
<table class="report report-course">
<caption class="sr">${_("A list of courses you have just registered for as a verified student")}</caption>
<thead>
<tr>
<th scope="col" >${_("Course")}</th>
<th scope="col" >${_("Status")}</th>
<th scope="col" ><span class="sr">${_("Options")}</span></th>
</tr>
</thead>
<tbody>
% for item, course in shoppingcart_items:
<tr>
<td>${item.line_desc}</td>
<td>
${_("Starts: {start_date}").format(start_date=course_start_date_text)}
</td>
<td class="options">
%if course_has_started:
<a class="action action-course" href="${course_root_url}">${_("Go to Course")}</a>
%else:
%endif
</td>
</tr>
% endfor
</tbody>
<tfoot>
<tr class="course-actions">
<td colspan="3">
<a class="action action-dashboard" href="${dashboard_url}">${_("Go to your Dashboard")}</a>
</td>
</tr>
</tfoot>
</table>
</div>
</li>
<li class="info-item verification-info">
<h4 class="title">${_("Verified Status")}</h4>
<div class="copy">
<p>${_("We have received your identification details to verify your identity. If there is a problem with any of the items, we will contact you to resubmit. You can now register for any of the verified certificate courses this semester without having to re-verify.")}</p>
<p>${_("The professor will ask you to periodically submit a new photo to verify your work during the course (usually at exam times).")}</p>
</div>
</li>
<li class="info-item payment-info">
<h4 class="title">${_("Payment Details")}</h4>
<div class="copy">
<p>${_("Please print this page for your records; it serves as your receipt. You will also receive an email with the same information.")}</p>
</div>
<div class="wrapper-report">
<table class="report report-receipt">
<thead>
<tr>
<th scope="col" >${_("Order No.")}</th>
<th scope="col" >${_("Description")}</th>
<th scope="col" >${_("Date")}</th>
<th scope="col" >${_("Description")}</th>
</tr>
</thead>
<tbody>
% for item, course in shoppingcart_items:
<tr>
% if item.status == "purchased":
<td>${order.id}</td>
<td>${item.line_desc}</td>
<td>${order.purchase_time.date().isoformat()}</td>
<td>${"{0:0.2f}".format(item.line_cost)} (${item.currency.upper()})</td>
% elif item.status == "refunded":
<td><del>${order.id}</del></td>
<td><del>${item.line_desc}</del></td>
<td><del>${order.purchase_time.date().isoformat()}</del></td>
<td><del>${"{0:0.2f}".format(item.line_cost)} (${item.currency.upper()})</del></td>
% endif
</tr>
% endfor
</tbody>
<tfoot>
<tr>
<th scope="row" class="total-label" colspan="1">${_("Total")}</th>
<td claass="total-value" colspan="3">
<span class="value-amount">${"{0:0.2f}".format(order.total_cost)} </span>
<span class="value-currency">(${item.currency.upper()})</span>
</td>
</tr>
</tfoot>
</table>
% if any_refunds:
<div class="msg msg-refunds">
<h4 class="title sr">Please Note:</h4>
<div class="copy">
## Translators: Please keep the "<del>" and "</del>" tags around your translation of the word "this" in your translation.
<p>${_("Note: items with strikethough like <del>this</del> have been refunded.")}</p>
</div>
</div>
% endif
</div>
<div class="copy">
<p>${_("Billed To")}:
<span class="name-first">${order.bill_to_first}</span> <span class="name-last">${order.bill_to_last}</span> (<span class="address-city">${order.bill_to_city}</span>, <span class="address-state">${order.bill_to_state}</span> <span class="address-postalcode">${order.bill_to_postalcode}</span> <span class="address-country">${order.bill_to_country.upper()}</span>)
</p>
</div>
</li>
<%doc>
<li class="info-item billing-info">
<h4 class="title">${_("Billing Information")}</h4>
<div class="wrapper-report">
<table class="report report-billing">
<thead>
<tr>
<th scope="col">${_("Billed To")}</th>
<th scope="col">${_("Billing Address")}</th>
<th scope="col">${_("Payment Method Type")}</th>
<th scope="col">${_("Payment Method Details")}</th>
</tr>
</thead>
<tbody>
<tr>
<td class="billing-to">
${order.bill_to_first} ${order.bill_to_last}
</td>
<td class="billing-address">
<span class="address-street1">${order.bill_to_street1}</span>
<span class="address-street2">${order.bill_to_street2}</span>
<span class="address-city">${order.bill_to_street2}</span>,
<span class="address-state">${order.bill_to_state}</span>
<span class="address-postalcode">${order.bill_to_postalcode}</span>
<span class="address-country">${order.bill_to_country.upper()}</span>
</td>
<td class="billing-methodtype">
${order.bill_to_cardtype}
</td>
<td class="method-details">
${order.bill_to_ccnum}
</td>
</tr>
</tbody>
<tfoot>
<tr>
<th scope="row" class="total-label" colspan="1">${_("Total")}</th>
<td class="total-value" colspan="3"><span class="value-amount">${"{0:0.2f}".format(order.total_cost)}</span> <span class="value-currency">(${item.currency.upper()})</span></td>
</tr>
</tfoot>
</table>
</div>
</li>
</%doc>
</ul>
</article>
</div>
</section>
</div>
</%block>
<%! 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