diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py
index 76cf81d6fb..6500060fbf 100644
--- a/lms/djangoapps/shoppingcart/models.py
+++ b/lms/djangoapps/shoppingcart/models.py
@@ -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 "
diff --git a/lms/djangoapps/shoppingcart/tests/test_models.py b/lms/djangoapps/shoppingcart/tests/test_models.py
index 1662fd5b08..f3240fa40b 100644
--- a/lms/djangoapps/shoppingcart/tests/test_models.py
+++ b/lms/djangoapps/shoppingcart/tests/test_models.py
@@ -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",
diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py
index bea96cb7c6..fb5606095c 100644
--- a/lms/djangoapps/shoppingcart/tests/test_views.py
+++ b/lms/djangoapps/shoppingcart/tests/test_views.py
@@ -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})
diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py
index f21a452819..383e3ce8b9 100644
--- a/lms/djangoapps/shoppingcart/views.py
+++ b/lms/djangoapps/shoppingcart/views.py
@@ -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)
diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py
index 350a2c543f..014e9db652 100644
--- a/lms/djangoapps/verify_student/tests/test_views.py
+++ b/lms/djangoapps/verify_student/tests/test_views.py
@@ -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()
diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py
index 9b40f00488..770efc871f 100644
--- a/lms/djangoapps/verify_student/views.py
+++ b/lms/djangoapps/verify_student/views.py
@@ -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.
diff --git a/lms/templates/shoppingcart/verified_cert_receipt.html b/lms/templates/shoppingcart/verified_cert_receipt.html
deleted file mode 100644
index be1068d49c..0000000000
--- a/lms/templates/shoppingcart/verified_cert_receipt.html
+++ /dev/null
@@ -1,263 +0,0 @@
-<%! 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:
-
-% endif
-
-
-
-
-
-
-
-
- ${_("Your Progress")}
-
-
- -
- 0
- ${_("Current Step: ")}${_("Intro")}
-
-
- -
- 1
- ${_("Take Photo")}
-
-
- -
- 2
- ${_("Take ID Photo")}
-
-
- -
- 3
- ${_("Review")}
-
-
- -
- 4
- ${_("Make Payment")}
-
-
- -
-
-
-
- ${_("Current Step: ")}${_("Confirmation")}
-
-
-
-
-
-
-
-
-
-
-
- ${_("Congratulations! You are now verified on ")} ${_(settings.PLATFORM_NAME)}.
-
-
-
${_("You are now registered as a verified student! Your registration details are below.")}
-
-
-
- -
-
${_("You are registered for:")}
-
-
-
- ${_("A list of courses you have just registered for as a verified student")}
-
-
- | ${_("Course")} |
- ${_("Status")} |
- ${_("Options")} |
-
-
-
-
- % for item, course in shoppingcart_items:
-
- | ${item.line_desc} |
-
- ${_("Starts: {start_date}").format(start_date=course_start_date_text)}
- |
-
- %if course_has_started:
- ${_("Go to Course")}
- %else:
- %endif
- |
-
- % endfor
-
-
-
- |
- ${_("Go to your Dashboard")}
- |
-
-
-
-
-
-
- -
-
${_("Verified Status")}
-
-
-
${_("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.")}
-
-
${_("The professor will ask you to periodically submit a new photo to verify your work during the course (usually at exam times).")}
-
-
-
- -
-
${_("Payment Details")}
-
-
-
${_("Please print this page for your records; it serves as your receipt. You will also receive an email with the same information.")}
-
-
-
-
-
-
- | ${_("Order No.")} |
- ${_("Description")} |
- ${_("Date")} |
- ${_("Description")} |
-
-
-
-
- % for item, course in shoppingcart_items:
-
- % if item.status == "purchased":
- | ${order.id} |
- ${item.line_desc} |
- ${order.purchase_time.date().isoformat()} |
- ${"{0:0.2f}".format(item.line_cost)} (${item.currency.upper()}) |
-
- % elif item.status == "refunded":
- ${order.id} |
- ${item.line_desc} |
- ${order.purchase_time.date().isoformat()} |
- ${"{0:0.2f}".format(item.line_cost)} (${item.currency.upper()}) |
- % endif
-
- % endfor
-
-
-
-
- | ${_("Total")} |
-
- ${"{0:0.2f}".format(order.total_cost)}
- (${item.currency.upper()})
- |
-
-
-
-
- % if any_refunds:
-
-
Please Note:
-
- ## Translators: Please keep the "
" and "" tags around your translation of the word "this" in your translation.
-
${_("Note: items with strikethough like this have been refunded.")}
-
-
- % endif
-
-
-
-
${_("Billed To")}:
- ${order.bill_to_first} ${order.bill_to_last} (${order.bill_to_city}, ${order.bill_to_state} ${order.bill_to_postalcode} ${order.bill_to_country.upper()})
-
-
-
-
- <%doc>
- -
-
${_("Billing Information")}
-
-
-
-
-
- | ${_("Billed To")} |
- ${_("Billing Address")} |
- ${_("Payment Method Type")} |
- ${_("Payment Method Details")} |
-
-
-
-
-
- |
- ${order.bill_to_first} ${order.bill_to_last}
- |
-
-
- ${order.bill_to_street1}
- ${order.bill_to_street2}
-
- ${order.bill_to_street2},
- ${order.bill_to_state}
- ${order.bill_to_postalcode}
-
- ${order.bill_to_country.upper()}
- |
-
-
- ${order.bill_to_cardtype}
- |
-
- ${order.bill_to_ccnum}
- |
-
-
-
-
-
- | ${_("Total")} |
- ${"{0:0.2f}".format(order.total_cost)} (${item.currency.upper()}) |
-
-
-
-
-
- %doc>
-
-
-
-
-
-
-%block>
diff --git a/lms/templates/verify_student/missed_verification_deadline.html b/lms/templates/verify_student/missed_verification_deadline.html
new file mode 100644
index 0000000000..5c69a3052d
--- /dev/null
+++ b/lms/templates/verify_student/missed_verification_deadline.html
@@ -0,0 +1,18 @@
+<%! 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">
+
+ ${_(
+ u"The verification deadline for {course_name} was {date}. "
+ u"Verification is no longer available."
+ ).format(
+ course_name=course.display_name,
+ date=deadline
+ )}
+
+%block>