Commit 42527ff3 by Vedran Karacic Committed by Vedran Karačić

Change enrollment code redemption to the receipt page.

SOL-2073
parent 641c1a6f
import datetime
import urllib
import ddt
import httpretty
import mock
import pytz
from django.conf import settings
from django.core.urlresolvers import reverse
......@@ -20,6 +22,9 @@ from ecommerce.coupons.views import voucher_is_valid
from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.extensions.api import exceptions
from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin
from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin
from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.fulfillment.status import ORDER
from ecommerce.extensions.test.factories import prepare_voucher
from ecommerce.extensions.voucher.utils import get_voucher_and_products_from_code
from ecommerce.tests.mixins import ApiMockMixin, LmsApiMockMixin
......@@ -31,6 +36,7 @@ Benefit = get_model('offer', 'Benefit')
Catalog = get_model('catalogue', 'Catalog')
Course = get_model('courses', 'Course')
Product = get_model('catalogue', 'Product')
Order = get_model('order', 'Order')
OrderLineVouchers = get_model('voucher', 'OrderLineVouchers')
StockRecord = get_model('partner', 'StockRecord')
Voucher = get_model('voucher', 'Voucher')
......@@ -262,6 +268,7 @@ class CouponOfferViewTests(ApiMockMixin, CouponMixin, CourseCatalogTestMixin, Lm
self.assertEqual(response.status_code, 200)
@ddt.ddt
class CouponRedeemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockMixin, TestCase):
redeem_url = reverse('coupons:redeem')
......@@ -279,7 +286,6 @@ class CouponRedeemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockMixin
self.stock_record = StockRecord.objects.get(product=self.seat)
self.catalog = Catalog.objects.create(partner=self.partner)
self.catalog.stock_records.add(StockRecord.objects.get(product=self.seat))
self.student_dashboard_url = get_lms_url(self.site.siteconfiguration.student_dashboard_url)
def redeem_url_with_params(self, code=COUPON_CODE):
""" Constructs the coupon redemption URL with the proper string query parameters. """
......@@ -297,12 +303,23 @@ class CouponRedeemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockMixin
self.assertEqual(Voucher.objects.filter(code=coupon_code).count(), 1)
return coupon_code
def assert_redemption_page_redirects(self, expected_url, target=200, code=COUPON_CODE):
def assert_redemption_page_redirects(self, expected_url, target=200, code=COUPON_CODE, query_string=''):
""" Verify redirect from redeem page to expected page. """
self.request.user = self.user
self.mock_enrollment_api(self.request, self.user, self.course.id, is_active=False, mode=self.course_mode)
response = self.client.get(self.redeem_url_with_params(code=code))
self.assertRedirects(response, expected_url, status_code=302, target_status_code=target)
if query_string:
order = Order.objects.latest()
expected_url = '{url}?{query_string}={order_num}'.format(
url=expected_url,
query_string=query_string,
order_num=order.number
)
self.assertRedirects(
response, expected_url, status_code=302, target_status_code=target, fetch_redirect_response=False
)
def test_login_required(self):
""" Users are required to login before accessing the view. """
......@@ -358,29 +375,75 @@ class CouponRedeemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockMixin
self.assert_redemption_page_redirects(expected_url)
@httpretty.activate
def test_basket_redirect_enrollment_code(self):
""" Verify the view redirects to LMS when an enrollment code is provided. """
@ddt.data(
(True, 'order_number'),
(False, 'orderNum')
)
@ddt.unpack
def test_basket_redirect_enrollment_code(self, ecomm_receipt, query_string):
""" Verify the view redirects to a receipt page when an enrollment code is provided. """
self.toggle_ecommerce_receipt_page(ecomm_receipt)
code = self.create_and_test_coupon_and_return_code(benefit_value=100, code='')
httpretty.register_uri(httpretty.GET, self.student_dashboard_url, status=status.HTTP_301_MOVED_PERMANENTLY)
receipt_page_url = get_receipt_page_url(self.site.siteconfiguration)
enrollment_api_url = '{root}{enrollment}'.format(
root=self.site.siteconfiguration.enrollment_api_url,
enrollment='enrollment'
)
httpretty.register_uri(httpretty.POST, enrollment_api_url, status=status.HTTP_200_OK)
httpretty.register_uri(httpretty.GET, receipt_page_url, status=status.HTTP_301_MOVED_PERMANENTLY)
self.mock_account_api(self.request, self.user.username, data={'is_active': True})
self.assert_redemption_page_redirects(
self.student_dashboard_url,
receipt_page_url,
target=status.HTTP_301_MOVED_PERMANENTLY,
code=code,
query_string=query_string
)
@httpretty.activate
@mock.patch.object(EdxOrderPlacementMixin, 'place_free_order')
def test_basket_redirect_enrollment_code_error(self, place_free_order):
""" Verify the view redirects to checkout error page when an order is not completed. """
code = self.create_and_test_coupon_and_return_code(benefit_value=100, code='')
self.mock_account_api(self.request, self.user.username, data={'is_active': True})
open_order = OrderFactory(status=ORDER.OPEN)
place_free_order.return_value = open_order
self.assert_redemption_page_redirects(
self.get_full_url(reverse('checkout:error')),
target=status.HTTP_301_MOVED_PERMANENTLY,
code=code
)
@httpretty.activate
def test_multiple_vouchers(self):
@ddt.data(
(True, 'order_number'),
(False, 'orderNum')
)
@ddt.unpack
def test_multiple_vouchers(self, ecomm_receipt, query_string):
""" Verify a redirect to LMS happens when a basket with already existing vouchers is used. """
self.toggle_ecommerce_receipt_page(ecomm_receipt)
receipt_page_url = get_receipt_page_url(self.site.siteconfiguration)
code = self.create_and_test_coupon_and_return_code(benefit_value=100, code='')
basket = Basket.get_basket(self.user, self.site)
basket.vouchers.add(Voucher.objects.get(code=code))
enrollment_api_url = '{root}{enrollment}'.format(
root=self.site.siteconfiguration.enrollment_api_url,
enrollment='enrollment'
)
httpretty.register_uri(httpretty.POST, enrollment_api_url, status=status.HTTP_200_OK)
self.mock_account_api(self.request, self.user.username, data={'is_active': True})
httpretty.register_uri(httpretty.GET, self.student_dashboard_url, status=status.HTTP_301_MOVED_PERMANENTLY)
httpretty.register_uri(httpretty.GET, receipt_page_url, status=status.HTTP_301_MOVED_PERMANENTLY)
self.assert_redemption_page_redirects(
self.student_dashboard_url,
receipt_page_url,
target=status.HTTP_301_MOVED_PERMANENTLY,
code=code
code=code,
query_string=query_string
)
@httpretty.activate
......
......@@ -2,6 +2,7 @@ from __future__ import unicode_literals
import csv
import logging
import time
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
......@@ -21,6 +22,8 @@ from ecommerce.coupons.decorators import login_required_for_credit
from ecommerce.extensions.api import exceptions
from ecommerce.extensions.basket.utils import prepare_basket
from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin
from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.fulfillment.status import ORDER
from ecommerce.extensions.voucher.utils import get_voucher_and_products_from_code
Applicator = get_class('offer.utils', 'Applicator')
......@@ -161,11 +164,22 @@ class CouponRedeemView(EdxOrderPlacementMixin, View):
basket = prepare_basket(request, product, voucher)
if basket.total_excl_tax == 0:
self.place_free_order(basket)
else:
return HttpResponseRedirect(reverse('basket:summary'))
return HttpResponseRedirect(request.site.siteconfiguration.student_dashboard_url)
order = self.place_free_order(basket)
# Waiting for the order to finish its async fulfillment.
# Every 2 seconds the order status is checked. If after three checks
# it's not complete or the status is an error, user is redirected
# to the checkout error page.
for __ in range(3): # pragma: no cover
order.refresh_from_db()
if order.status == ORDER.COMPLETE:
return HttpResponseRedirect(get_receipt_page_url(self.request.site.siteconfiguration, order.number))
elif order.status == ORDER.FULFILLMENT_ERROR:
return HttpResponseRedirect(reverse('checkout:error'))
time.sleep(2) # pragma: no cover
return HttpResponseRedirect(reverse('checkout:error'))
return HttpResponseRedirect(reverse('basket:summary'))
class EnrollmentCodeCsvView(View):
......
......@@ -74,8 +74,8 @@ class OrderListViewTests(AccessTokenMixin, ThrottlingMixin, TestCase):
content = json.loads(response.content)
self.assertEqual(content['count'], 2)
self.assertEqual(content['results'][0]['number'], unicode(order_2.number))
self.assertEqual(content['results'][1]['number'], unicode(order.number))
self.assertEqual(content['results'][0]['number'], unicode(order.number))
self.assertEqual(content['results'][1]['number'], unicode(order_2.number))
def test_with_other_users_orders(self):
""" The view should only return orders for the authenticated users. """
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('order', '0011_auto_20161025_1446'),
]
operations = [
migrations.AlterModelOptions(
name='historicalorder',
options={'ordering': ('-history_date', '-history_id'), 'get_latest_by': 'history_date', 'verbose_name': 'historical order'},
),
migrations.AlterModelOptions(
name='order',
options={'get_latest_by': 'date_placed'},
),
]
......@@ -21,6 +21,9 @@ class Order(AbstractOrder):
return any(line.product.get_product_class().name == 'Coupon' for line in
self.basket.all_lines())
class Meta(object):
get_latest_by = 'date_placed'
class PaymentEvent(AbstractPaymentEvent):
processor_name = models.CharField(_("Payment Processor"), max_length=32, blank=True, null=True)
......
......@@ -150,6 +150,19 @@ define([
expect(view.collection.previousPage).toHaveBeenCalled();
});
it('should disable the enrollment button', function() {
var ev = $.Event('click');
$('body').append('<a class="btn-success redeem-enrollment"></a>');
view.disableReedemEnrollmentBtn(ev);
expect(ev.isDefaultPrevented()).toBeFalsy();
expect($('.redeem-enrollment').attr('disabled')).toEqual('disabled');
expect($('.redeem-enrollment').hasClass('btn-default')).toBeTruthy();
expect($('.redeem-enrollment').hasClass('btn-success')).toBeFalsy();
view.disableReedemEnrollmentBtn(ev);
expect(ev.isDefaultPrevented()).toBeTruthy();
});
it('should create list item', function() {
var value = view.createListItem(1, false),
string = '<li class="page-item">' +
......
......@@ -24,7 +24,8 @@ define([
events: {
'click .prev': 'previous',
'click .next': 'next',
'click .page-number': 'goToPage'
'click .page-number': 'goToPage',
'click .redeem-enrollment': 'disableReedemEnrollmentBtn'
},
initialize: function (options) {
......@@ -32,6 +33,18 @@ define([
this.code = options.code;
},
disableReedemEnrollmentBtn: function(event) {
var btn = $('.redeem-enrollment');
if (btn.attr('disabled')) {
event.preventDefault();
} else {
btn.text('Enrolling...')
.attr('disabled', true)
.removeClass('btn-success')
.addClass('btn-default');
}
},
changePage: function() {
this.$el.html(
this.template({
......
......@@ -29,7 +29,6 @@
// views
// --------------------
@import 'views/basket';
@import 'views/cancel_error';
@import 'views/credit';
@import 'views/course_admin';
@import 'views/coupon_admin';
......
.receipt-cancel-error {
font-family: $font-family-sans-serif;
h1 {
font-weight: 600;
margin-bottom: 30px;
text-align: center;
}
.nav-link {
border-bottom: none;
text-decoration: none !important;
&:active, &:hover, &:focus {
border-bottom: 1px dotted #0079bc;
}
}
p {
font-size: large;
}
}
\ No newline at end of file
......@@ -310,4 +310,8 @@
color: #6B6969;
}
.btn-default[disabled]:hover,
.btn-default[disabled]:focus {
color: #333;
}
}
......@@ -70,8 +70,7 @@
<% } else { %>
<% if (isEnrollmentCode) { %>
<a href="/coupons/redeem/?code=<%= code %>&sku=<%= course.attributes.stockrecords.partner_sku %>"
id="RedeemEnrollment"
class="btn btn-success"><%- gettext('Enroll Now') %></a>
class="btn btn-success redeem-enrollment"><%- gettext('Enroll Now') %></a>
<% } else { %>
<a href="/coupons/redeem/?code=<%= code %>&sku=<%= course.attributes.stockrecords.partner_sku %>"
id="PurchaseCertificate"
......
......@@ -4,23 +4,27 @@
{% block title %}
{% trans "Checkout Error" %}
{% endblock title %}
{% endblock %}
{% block navbar %}
{% include 'edx/partials/_student_navbar.html' %}
{% endblock navbar %}
{% endblock %}
{% block content %}
<div class="container content-wrapper receipt-cancel-error">
<h1>{% trans "Checkout Error" %}</h1>
<p>{% blocktrans %} An error has occurred with your payment. <b>You have not been charged.</b>
{% endblocktrans %}</p>
<p>{% with "<a class='nav-link' href='mailto:"|add:payment_support_email|add:"'>"|safe as start_link %}
{% blocktrans with end_link="</a>"|safe %}
Please try to submit your payment again. If this problem persists, contact {{ start_link }}
{{ payment_support_email }}{{ end_link }}.
{% endblocktrans %}
{% endwith %}</p>
<div id="error-message">
<div class="container">
<div class="depth depth-2 message-error-content">
<h1>{% trans "Checkout Error" %}</h1>
<h3>{{ error }}</h3>
<p>{% blocktrans %} An error has occurred with your payment. <b>You have not been charged.</b>
{% endblocktrans %}</p>
<p>{% with "<a class='nav-link' href='mailto:"|add:payment_support_email|add:"'>"|safe as start_link %}
{% blocktrans with end_link="</a>"|safe %}
Please try to submit your payment again. If this problem persists, contact {{ start_link }}
{{ payment_support_email }}{{ end_link }}.
{% endblocktrans %}
{% endwith %}</p>
</div>
</div>
</div>
{% endblock content %}
{% endblock %}
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