Commit 80141cec by Marko Jevtić Committed by GitHub

Merge pull request #978 from edx/mjevtic/SOL-1954

[SOL-1954, SOL-1955, SOL-1956] New Receipt Page - Frontend
parents 57caf7c1 03e7d21d
...@@ -11,23 +11,23 @@ ...@@ -11,23 +11,23 @@
], ],
"dependencies": { "dependencies": {
"backbone": "~1.2.1", "backbone": "~1.2.1",
"backbone-relational": "~0.9.0",
"backbone-route-filter": "~0.1.2",
"backbone-super": "~1.0.4",
"backbone-validation": "~0.11.5",
"backbone.stickit": "~0.9.2",
"bootstrap-sass": "~3.3.4", "bootstrap-sass": "~3.3.4",
"jquery": "~1.11.3",
"js-cookie": "~2.1.2",
"requirejs": "~2.1.15",
"underscore": "~1.8.2",
"bootstrapaccessibilityplugin": "~1.0.4", "bootstrapaccessibilityplugin": "~1.0.4",
"datatables": "1.10.10", "datatables": "1.10.10",
"fontawesome": "~4.3.0",
"edx-ux-pattern-library": "https://github.com/edx/ux-pattern-library.git#82fa1c823bc322ba8b0742f0c546a89b0c69e952", "edx-ux-pattern-library": "https://github.com/edx/ux-pattern-library.git#82fa1c823bc322ba8b0742f0c546a89b0c69e952",
"text": "~2.0.14", "fontawesome": "~4.3.0",
"jquery": "~1.11.3",
"js-cookie": "~2.1.2",
"moment": "~2.10.3", "moment": "~2.10.3",
"pikaday": "https://github.com/owenmead/Pikaday.git#1.4.0", "pikaday": "https://github.com/owenmead/Pikaday.git#1.4.0",
"underscore.string": "~3.1.1", "requirejs": "~2.1.15",
"backbone-super": "~1.0.4", "text": "~2.0.14",
"backbone-route-filter": "~0.1.2", "underscore": "~1.8.2",
"backbone-relational": "~0.9.0", "underscore.string": "~3.1.1"
"backbone-validation": "~0.11.5",
"backbone.stickit": "~0.9.2"
} }
} }
...@@ -35,6 +35,10 @@ ...@@ -35,6 +35,10 @@
{ {
name: 'js/apps/basket_app', name: 'js/apps/basket_app',
exclude: ['js/common'] exclude: ['js/common']
},
{
name: 'js/pages/receipt_page',
exclude: ['js/common']
} }
] ]
}) })
...@@ -485,6 +485,7 @@ class User(AbstractUser): ...@@ -485,6 +485,7 @@ class User(AbstractUser):
cache.set(cache_key, verification, cache_timeout) cache.set(cache_key, verification, cache_timeout)
return verification return verification
except HttpNotFoundError: except HttpNotFoundError:
log.debug('No verification data found for [%s]', self.username)
return False return False
except (ConnectionError, SlumberBaseException, Timeout): except (ConnectionError, SlumberBaseException, Timeout):
msg = 'Failed to retrieve verification status details for [{username}]'.format(username=self.username) msg = 'Failed to retrieve verification status details for [{username}]'.format(username=self.username)
......
from django import template from django import template
from django.conf import settings from django.conf import settings
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from opaque_keys.edx.keys import CourseKey
register = template.Library() register = template.Library()
...@@ -38,6 +39,20 @@ def do_captureas(parser, token): ...@@ -38,6 +39,20 @@ def do_captureas(parser, token):
return CaptureasNode(nodelist, args) return CaptureasNode(nodelist, args)
@register.filter(name='course_organization')
def course_organization(course_key):
"""
Retrieve course organization from course key.
Arguments:
course_key (str): Course key.
Returns:
str: Course organization.
"""
return CourseKey.from_string(course_key).org
class CaptureasNode(template.Node): class CaptureasNode(template.Node):
def __init__(self, nodelist, varname): def __init__(self, nodelist, varname):
self.nodelist = nodelist self.nodelist = nodelist
......
...@@ -37,3 +37,11 @@ class CoreExtrasTests(TestCase): ...@@ -37,3 +37,11 @@ class CoreExtrasTests(TestCase):
def test_captureas_unicode(self): def test_captureas_unicode(self):
self.assertTextCaptured(u'★❤') self.assertTextCaptured(u'★❤')
def test_course_organization(self):
course_id = 'course-v1:edX+Course+100'
template = Template(
"{% load core_extras %}"
"{{ course_id|course_organization }}"
)
self.assertEqual(template.render(Context({'course_id': course_id})), 'edX')
...@@ -7,12 +7,14 @@ class CheckoutApplication(app.CheckoutApplication): ...@@ -7,12 +7,14 @@ class CheckoutApplication(app.CheckoutApplication):
free_checkout = get_class('checkout.views', 'FreeCheckoutView') free_checkout = get_class('checkout.views', 'FreeCheckoutView')
cancel_checkout = get_class('checkout.views', 'CancelCheckoutView') cancel_checkout = get_class('checkout.views', 'CancelCheckoutView')
checkout_error = get_class('checkout.views', 'CheckoutErrorView') checkout_error = get_class('checkout.views', 'CheckoutErrorView')
receipt_response = get_class('checkout.views', 'ReceiptResponseView')
def get_urls(self): def get_urls(self):
urls = [ urls = [
url(r'^free-checkout/$', self.free_checkout.as_view(), name='free-checkout'), url(r'^free-checkout/$', self.free_checkout.as_view(), name='free-checkout'),
url(r'^cancel-checkout/$', self.cancel_checkout.as_view(), name='cancel-checkout'), url(r'^cancel-checkout/$', self.cancel_checkout.as_view(), name='cancel-checkout'),
url(r'^error/', self.checkout_error.as_view(), name='error'), url(r'^error/', self.checkout_error.as_view(), name='error'),
url(r'^receipt/', self.receipt_response.as_view(), name='receipt'),
url(r'^$', self.index_view.as_view(), name='index'), url(r'^$', self.index_view.as_view(), name='index'),
...@@ -36,12 +38,10 @@ class CheckoutApplication(app.CheckoutApplication): ...@@ -36,12 +38,10 @@ class CheckoutApplication(app.CheckoutApplication):
url(r'payment-details/$', url(r'payment-details/$',
self.payment_details_view.as_view(), name='payment-details'), self.payment_details_view.as_view(), name='payment-details'),
# Preview and thankyou # Preview
url(r'preview/$', url(r'preview/$',
self.payment_details_view.as_view(preview=True), self.payment_details_view.as_view(preview=True),
name='preview'), name='preview'),
url(r'thank-you/$', self.thankyou_view.as_view(),
name='thank-you'),
] ]
return self.post_process_urls(urls) return self.post_process_urls(urls)
......
...@@ -22,7 +22,7 @@ class SignalTests(CourseCatalogTestMixin, TestCase): ...@@ -22,7 +22,7 @@ class SignalTests(CourseCatalogTestMixin, TestCase):
super(SignalTests, self).setUp() super(SignalTests, self).setUp()
self.user = self.create_user() self.user = self.create_user()
self.request.user = self.user self.request.user = self.user
self.site.siteconfiguration.enable_otto_receipt_page = True self.toggle_ecommerce_receipt_page(True)
toggle_switch('ENABLE_NOTIFICATIONS', True) toggle_switch('ENABLE_NOTIFICATIONS', True)
def prepare_order(self, seat_type, credit_provider_id=None): def prepare_order(self, seat_type, credit_provider_id=None):
...@@ -90,7 +90,7 @@ class SignalTests(CourseCatalogTestMixin, TestCase): ...@@ -90,7 +90,7 @@ class SignalTests(CourseCatalogTestMixin, TestCase):
credit_provider_name=credit_provider_name, credit_provider_name=credit_provider_name,
platform_name=self.site.name, platform_name=self.site.name,
receipt_url=self.site.siteconfiguration.build_ecommerce_url( receipt_url=self.site.siteconfiguration.build_ecommerce_url(
'{}{}'.format(settings.RECEIPT_PAGE_PATH, order.number) '{}?order_number={}'.format(settings.RECEIPT_PAGE_PATH, order.number)
) )
) )
) )
......
import urllib
from decimal import Decimal from decimal import Decimal
from django.core.urlresolvers import reverse import ddt
import httpretty import httpretty
from django.conf import settings
from django.core.urlresolvers import reverse
from oscar.core.loading import get_model from oscar.core.loading import get_model
from oscar.test import newfactories as factories from oscar.test import newfactories as factories
from ecommerce.core.url_utils import get_lms_url from ecommerce.coupons.tests.mixins import CourseCatalogMockMixin
from ecommerce.extensions.checkout.exceptions import BasketNotFreeError from ecommerce.extensions.checkout.exceptions import BasketNotFreeError
from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.checkout.views import ReceiptResponseView
from ecommerce.extensions.refund.tests.mixins import RefundTestMixin
from ecommerce.tests.mixins import LmsApiMockMixin
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
Order = get_model('order', 'Order') Order = get_model('order', 'Order')
...@@ -20,6 +27,7 @@ class FreeCheckoutViewTests(TestCase): ...@@ -20,6 +27,7 @@ class FreeCheckoutViewTests(TestCase):
super(FreeCheckoutViewTests, self).setUp() super(FreeCheckoutViewTests, self).setUp()
self.user = self.create_user() self.user = self.create_user()
self.client.login(username=self.user.username, password=self.password) self.client.login(username=self.user.username, password=self.password)
self.toggle_ecommerce_receipt_page(True)
def prepare_basket(self, price): def prepare_basket(self, price):
""" Helper function that creates a basket and adds a product with set price to it. """ """ Helper function that creates a basket and adds a product with set price to it. """
...@@ -46,7 +54,24 @@ class FreeCheckoutViewTests(TestCase): ...@@ -46,7 +54,24 @@ class FreeCheckoutViewTests(TestCase):
""" Verify redirect to the receipt page. """ """ Verify redirect to the receipt page. """
self.prepare_basket(0) self.prepare_basket(0)
self.assertEqual(Order.objects.count(), 0) self.assertEqual(Order.objects.count(), 0)
receipt_page = get_lms_url('/commerce/checkout/receipt')
response = self.client.get(self.path)
self.assertEqual(Order.objects.count(), 1)
order = Order.objects.first()
expected_url = get_receipt_page_url(
order_number=order.number,
site_configuration=order.site.siteconfiguration
)
self.assertRedirects(response, expected_url, fetch_redirect_response=False)
@httpretty.activate
def test_redirect_to_lms_receipt(self):
""" Verify that disabling the otto_receipt_page switch redirects to the LMS receipt page. """
self.toggle_ecommerce_receipt_page(False)
self.prepare_basket(0)
self.assertEqual(Order.objects.count(), 0)
receipt_page = self.site.siteconfiguration.build_lms_url('/commerce/checkout/receipt')
response = self.client.get(self.path) response = self.client.get(self.path)
self.assertEqual(Order.objects.count(), 1) self.assertEqual(Order.objects.count(), 1)
...@@ -121,3 +146,163 @@ class CheckoutErrorViewTests(TestCase): ...@@ -121,3 +146,163 @@ class CheckoutErrorViewTests(TestCase):
self.assertEqual( self.assertEqual(
response.context['payment_support_email'], self.request.site.siteconfiguration.payment_support_email response.context['payment_support_email'], self.request.site.siteconfiguration.payment_support_email
) )
@ddt.ddt
class ReceiptResponseViewTests(CourseCatalogMockMixin, LmsApiMockMixin, RefundTestMixin, TestCase):
"""
Tests for the receipt view.
"""
path = reverse('checkout:receipt')
def setUp(self):
super(ReceiptResponseViewTests, self).setUp()
self.user = self.create_user()
self.client.login(username=self.user.username, password=self.password)
def _get_receipt_response(self, order_number):
"""
Helper function for getting the receipt page response for an order.
Arguments:
order_number (str): Number of Order for which the Receipt Page should be opened.
Returns:
response (Response): Response object that's returned by a ReceiptResponseView
"""
url = '{path}?order_number={order_number}'.format(path=self.path, order_number=order_number)
return self.client.get(url)
def _visit_receipt_page_with_another_user(self, order, user):
"""
Helper function for logging in with another user and going to the Receipt Page.
Arguments:
order (Order): Order for which the Receipt Page should be opened.
user (User): User that's logging in.
Returns:
response (Response): Response object that's returned by a ReceiptResponseView
"""
self.client.logout()
self.client.login(username=user.username, password=self.password)
return self._get_receipt_response(order.number)
def _create_order_for_receipt(self, user, credit=False):
"""
Helper function for creating an order and mocking verification status API response.
Arguments:
user (User): User that's trying to visit the Receipt page.
credit (bool): Indicates whether or not the product is a Credit Course Seat.
Returns:
order (Order): Order for which the Receipt is requested.
"""
self.mock_verification_status_api(
self.site,
user,
status=200,
is_verified=False
)
return self.create_order(credit=credit)
def test_login_required_get_request(self):
""" The view should redirect to the login page if the user is not logged in. """
self.client.logout()
response = self.client.get(self.path)
testserver_login_url = self.get_full_url(reverse(settings.LOGIN_URL))
expected_url = '{path}?next={next}'.format(path=testserver_login_url, next=urllib.quote(self.path))
self.assertRedirects(response, expected_url, target_status_code=302)
def test_get_receipt_for_nonexisting_order(self):
""" The view should return 404 status if the Order is not found. """
order_number = 'ABC123'
response = self._get_receipt_response(order_number)
self.assertEqual(response.status_code, 404)
def test_get_payment_method_no_source(self):
""" Payment method should be None when an Order has no Payment source. """
order = self.create_order()
payment_method = ReceiptResponseView().get_payment_method(order)
self.assertEqual(payment_method, None)
def test_get_payment_method_source_type(self):
"""
Source Type name should be displayed as the Payment method
when the credit card wasn't used to purchase a product.
"""
order = self.create_order()
source = factories.SourceFactory(order=order)
payment_method = ReceiptResponseView().get_payment_method(order)
self.assertEqual(payment_method, source.source_type.name)
def test_get_payment_method_credit_card_purchase(self):
"""
Credit card type and Source label should be displayed as the Payment method
when a Credit card was used to purchase a product.
"""
order = self.create_order()
source = factories.SourceFactory(order=order, card_type='Dummy Card', label='Test')
payment_method = ReceiptResponseView().get_payment_method(order)
self.assertEqual(payment_method, '{} {}'.format(source.card_type, source.label))
@httpretty.activate
def test_get_receipt_for_existing_order(self):
""" Order owner should be able to see the Receipt Page."""
order = self._create_order_for_receipt(self.user)
response = self._get_receipt_response(order.number)
context_data = {
'payment_method': None,
'fire_tracking_events': False,
'display_credit_messaging': False,
}
self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset(context_data, response.context_data)
@httpretty.activate
def test_get_receipt_for_existing_order_as_staff_user(self):
""" Staff users can preview Receipts for all Orders."""
staff_user = self.create_user(is_staff=True)
order = self._create_order_for_receipt(staff_user)
response = self._visit_receipt_page_with_another_user(order, staff_user)
context_data = {
'payment_method': None,
'fire_tracking_events': False,
'display_credit_messaging': False,
}
self.assertEqual(response.status_code, 200)
self.assertDictContainsSubset(context_data, response.context_data)
@httpretty.activate
def test_get_receipt_for_existing_order_user_not_owner(self):
""" Users that don't own the Order shouldn't be able to see the Receipt. """
other_user = self.create_user()
order = self._create_order_for_receipt(other_user)
response = self._visit_receipt_page_with_another_user(order, other_user)
context_data = {'order_history_url': self.site.siteconfiguration.build_lms_url('account/settings')}
self.assertEqual(response.status_code, 404)
self.assertDictContainsSubset(context_data, response.context_data)
@httpretty.activate
def test_order_data_for_credit_seat(self):
""" Ensure that the context is updated with Order data. """
order = self.create_order(credit=True)
self.mock_verification_status_api(
self.site,
self.user,
status=200,
is_verified=True
)
seat = order.lines.first().product
body = {'display_name': 'Hogwarts'}
response = self._get_receipt_response(order.number)
body['course_key'] = seat.attr.course_key
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context_data['display_credit_messaging'])
import logging import logging
import urllib
from babel.numbers import format_currency from babel.numbers import format_currency
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse
from django.utils.translation import get_language, to_locale from django.utils.translation import get_language, to_locale
from edx_rest_api_client.client import EdxRestApiClient from edx_rest_api_client.client import EdxRestApiClient
from requests.exceptions import ConnectionError, Timeout from requests.exceptions import ConnectionError, Timeout
...@@ -42,15 +44,15 @@ def get_receipt_page_url(site_configuration, order_number=None): ...@@ -42,15 +44,15 @@ def get_receipt_page_url(site_configuration, order_number=None):
str: Receipt page URL. str: Receipt page URL.
""" """
if site_configuration.enable_otto_receipt_page: if site_configuration.enable_otto_receipt_page:
return site_configuration.build_ecommerce_url('{base_url}{order_number}'.format( base_url = site_configuration.build_ecommerce_url(reverse('checkout:receipt'))
base_url=settings.RECEIPT_PAGE_PATH, params = urllib.urlencode({'order_number': order_number}) if order_number else ''
order_number=order_number if order_number else '' else:
)) base_url = site_configuration.build_lms_url('/commerce/checkout/receipt')
return site_configuration.build_lms_url( params = urllib.urlencode({'orderNum': order_number}) if order_number else ''
'{base_url}{order_number}'.format(
base_url='/commerce/checkout/receipt', return '{base_url}{params}'.format(
order_number='?orderNum={}'.format(order_number) if order_number else '' base_url=base_url,
) params='?{params}'.format(params=params) if params else ''
) )
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from decimal import Decimal from decimal import Decimal
import logging
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.generic import RedirectView, TemplateView from django.views.generic import RedirectView, TemplateView
...@@ -19,7 +19,7 @@ from ecommerce.extensions.checkout.utils import get_receipt_page_url ...@@ -19,7 +19,7 @@ from ecommerce.extensions.checkout.utils import get_receipt_page_url
Applicator = get_class('offer.utils', 'Applicator') Applicator = get_class('offer.utils', 'Applicator')
Basket = get_model('basket', 'Basket') Basket = get_model('basket', 'Basket')
logger = logging.getLogger(__name__) Order = get_model('order', 'Order')
class FreeCheckoutView(EdxOrderPlacementMixin, RedirectView): class FreeCheckoutView(EdxOrderPlacementMixin, RedirectView):
...@@ -50,7 +50,6 @@ class FreeCheckoutView(EdxOrderPlacementMixin, RedirectView): ...@@ -50,7 +50,6 @@ class FreeCheckoutView(EdxOrderPlacementMixin, RedirectView):
) )
order = self.place_free_order(basket) order = self.place_free_order(basket)
receipt_path = get_receipt_page_url( receipt_path = get_receipt_page_url(
order_number=order.number, order_number=order.number,
site_configuration=order.site.siteconfiguration site_configuration=order.site.siteconfiguration
...@@ -118,3 +117,84 @@ class CheckoutErrorView(TemplateView): ...@@ -118,3 +117,84 @@ class CheckoutErrorView(TemplateView):
'payment_support_email': self.request.site.siteconfiguration.payment_support_email, 'payment_support_email': self.request.site.siteconfiguration.payment_support_email,
}) })
return context return context
class ReceiptResponseView(ThankYouView):
""" Handles behavior needed to display an order receipt. """
template_name = 'edx/checkout/receipt.html'
@method_decorator(csrf_exempt)
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
""" Customers should only be able to view their receipts when logged in. """
return super(ReceiptResponseView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
try:
return super(ReceiptResponseView, self).get(request, *args, **kwargs)
except Http404:
self.template_name = 'edx/checkout/receipt_not_found.html'
context = {
'order_history_url': request.site.siteconfiguration.build_lms_url('account/settings'),
}
return self.render_to_response(context=context, status=404)
def get_context_data(self, **kwargs):
context = super(ReceiptResponseView, self).get_context_data(**kwargs)
order = context[self.context_object_name]
context.update({
'payment_method': self.get_payment_method(order),
'fire_tracking_events': self.request.session.pop('fire_tracking_events', False),
'display_credit_messaging': self.order_contains_credit_seat(order),
})
context.update(self.get_order_verification_context(order))
return context
def get_object(self):
kwargs = {
'number': self.request.GET['order_number'],
'site': self.request.site,
}
user = self.request.user
if not user.is_staff:
kwargs['user'] = user
return get_object_or_404(Order, **kwargs)
def get_payment_method(self, order):
source = order.sources.first()
if source:
if source.card_type:
return '{type} {number}'.format(
type=source.get_card_type_display(),
number=source.label
)
return source.source_type.name
return None
def order_contains_credit_seat(self, order):
for line in order.lines.all():
if getattr(line.product.attr, 'credit_provider', None):
return True
return False
def get_order_verification_context(self, order):
context = {}
verified_course_id = None
# NOTE: Only display verification and credit completion details to the user who actually placed the order.
if self.request.user == order.user:
for line in order.lines.all():
product = line.product
if not verified_course_id and getattr(product.attr, 'id_verification_required', False):
verified_course_id = product.attr.course_key
if verified_course_id:
context.update({
'verified_course_id': verified_course_id,
'user_verified': self.request.user.is_verified(self.request.site),
})
return context
...@@ -443,7 +443,7 @@ class EnrollmentCodeFulfillmentModuleTests(CourseCatalogTestMixin, TestCase): ...@@ -443,7 +443,7 @@ class EnrollmentCodeFulfillmentModuleTests(CourseCatalogTestMixin, TestCase):
def setUp(self): def setUp(self):
super(EnrollmentCodeFulfillmentModuleTests, self).setUp() super(EnrollmentCodeFulfillmentModuleTests, self).setUp()
toggle_switch(ENROLLMENT_CODE_SWITCH, True) toggle_switch(ENROLLMENT_CODE_SWITCH, True)
self.site.siteconfiguration.enable_otto_receipt_page = True self.toggle_ecommerce_receipt_page(True)
course = CourseFactory() course = CourseFactory()
course.create_or_update_seat('verified', True, 50, self.partner, create_enrollment_code=True) course.create_or_update_seat('verified', True, 50, self.partner, create_enrollment_code=True)
enrollment_code = Product.objects.get(product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME) enrollment_code = Product.objects.get(product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME)
......
from django import template
from ecommerce.extensions.offer.utils import format_benefit_value
register = template.Library()
@register.filter(name='benefit_discount')
def benefit_discount(benefit):
"""
Format benefit value for display based on the benefit type.
Example:
'100%' if benefit.value == 100.00 and benefit.type == 'Percentage'
'$100.00' if benefit.value == 100.00 and benefit.type == 'Absolute'
Arguments:
benefit (Benefit): Voucher's Benefit.
Returns:
str: String value containing formatted benefit value and type.
"""
return format_benefit_value(benefit)
from django.template import Context, Template
from oscar.core.loading import get_model
from oscar.test.factories import BenefitFactory
from ecommerce.tests.testcases import TestCase
Benefit = get_model('offer', 'Benefit')
class OfferTests(TestCase):
def test_benefit_discount(self):
benefit = BenefitFactory(type=Benefit.PERCENTAGE, value=35.00)
template = Template(
"{% load offer_tags %}"
"{{ benefit|benefit_discount }}"
)
self.assertEqual(template.render(Context({'benefit': benefit})), '35%')
...@@ -31,7 +31,7 @@ def get_discount_percentage(discount_value, product_price): ...@@ -31,7 +31,7 @@ def get_discount_percentage(discount_value, product_price):
Returns: Returns:
float: Discount percentage float: Discount percentage
""" """
return discount_value / product_price * 100 return discount_value / product_price * 100 if product_price > 0 else 0.0
def get_discount_value(discount_percentage, product_price): def get_discount_value(discount_percentage, product_price):
......
...@@ -2,22 +2,23 @@ ...@@ -2,22 +2,23 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import six import six
from django.utils.translation import ugettext_lazy as _
CARD_TYPES = { CARD_TYPES = {
'american_express': { 'american_express': {
'display_name': 'American Express', 'display_name': _('American Express'),
'cybersource_code': '003' 'cybersource_code': '003'
}, },
'discover': { 'discover': {
'display_name': 'Discover', 'display_name': _('Discover'),
'cybersource_code': '004' 'cybersource_code': '004'
}, },
'mastercard': { 'mastercard': {
'display_name': 'MasterCard', 'display_name': _('MasterCard'),
'cybersource_code': '002' 'cybersource_code': '002'
}, },
'visa': { 'visa': {
'display_name': 'Visa', 'display_name': _('Visa'),
'cybersource_code': '001' 'cybersource_code': '001'
}, },
} }
......
...@@ -31,7 +31,7 @@ class CybersourceTests(CybersourceMixin, PaymentProcessorTestCaseMixin, TestCase ...@@ -31,7 +31,7 @@ class CybersourceTests(CybersourceMixin, PaymentProcessorTestCaseMixin, TestCase
def setUp(self): def setUp(self):
super(CybersourceTests, self).setUp() super(CybersourceTests, self).setUp()
self.site.siteconfiguration.enable_otto_receipt_page = True self.toggle_ecommerce_receipt_page(True)
self.basket.site = self.site self.basket.site = self.site
@freeze_time('2016-01-01') @freeze_time('2016-01-01')
......
...@@ -171,7 +171,7 @@ class PaypalTests(PaypalMixin, PaymentProcessorTestCaseMixin, TestCase): ...@@ -171,7 +171,7 @@ class PaypalTests(PaypalMixin, PaymentProcessorTestCaseMixin, TestCase):
""" """
Ensures that when the otto_receipt_page waffle switch is enabled, the processor uses the new receipt page. Ensures that when the otto_receipt_page waffle switch is enabled, the processor uses the new receipt page.
""" """
self.site.siteconfiguration.enable_otto_receipt_page = True self.toggle_ecommerce_receipt_page(True)
assert self._get_receipt_url() == self.site.siteconfiguration.build_ecommerce_url(settings.RECEIPT_PAGE_PATH) assert self._get_receipt_url() == self.site.siteconfiguration.build_ecommerce_url(settings.RECEIPT_PAGE_PATH)
def test_switch_disabled_lms_url(self): def test_switch_disabled_lms_url(self):
......
...@@ -42,7 +42,7 @@ class CybersourceNotifyViewTests(CybersourceMixin, PaymentEventsMixin, TestCase) ...@@ -42,7 +42,7 @@ class CybersourceNotifyViewTests(CybersourceMixin, PaymentEventsMixin, TestCase)
def setUp(self): def setUp(self):
super(CybersourceNotifyViewTests, self).setUp() super(CybersourceNotifyViewTests, self).setUp()
self.site.siteconfiguration.enable_otto_receipt_page = True self.toggle_ecommerce_receipt_page(True)
self.user = factories.UserFactory() self.user = factories.UserFactory()
self.billing_address = self.make_billing_address() self.billing_address = self.make_billing_address()
......
...@@ -6,12 +6,11 @@ import six ...@@ -6,12 +6,11 @@ import six
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction from django.db import transaction
from django.http import HttpResponse, JsonResponse from django.http import JsonResponse, HttpResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.generic import FormView from django.views.generic import FormView, View
from django.views.generic import View
from oscar.apps.partner import strategy from oscar.apps.partner import strategy
from oscar.apps.payment.exceptions import PaymentError, UserCancelled, TransactionDeclined from oscar.apps.payment.exceptions import PaymentError, UserCancelled, TransactionDeclined
from oscar.core.loading import get_class, get_model from oscar.core.loading import get_class, get_model
......
...@@ -27,12 +27,21 @@ class RefundTestMixin(CourseCatalogTestMixin): ...@@ -27,12 +27,21 @@ class RefundTestMixin(CourseCatalogTestMixin):
self.course, __ = Course.objects.get_or_create(id=u'edX/DemoX/Demo_Course', name=u'edX Demó Course') self.course, __ = Course.objects.get_or_create(id=u'edX/DemoX/Demo_Course', name=u'edX Demó Course')
self.honor_product = self.course.create_or_update_seat('honor', False, 0, self.partner) self.honor_product = self.course.create_or_update_seat('honor', False, 0, self.partner)
self.verified_product = self.course.create_or_update_seat('verified', True, 10, self.partner) self.verified_product = self.course.create_or_update_seat('verified', True, 10, self.partner)
self.credit_product = self.course.create_or_update_seat(
'credit',
True,
100,
self.partner,
credit_provider='HGW'
)
def create_order(self, user=None, multiple_lines=False, free=False, status=ORDER.COMPLETE): def create_order(self, user=None, credit=False, multiple_lines=False, free=False, status=ORDER.COMPLETE):
user = user or self.user user = user or self.user
basket = BasketFactory(owner=user) basket = BasketFactory(owner=user)
if multiple_lines: if credit:
basket.add_product(self.credit_product)
elif multiple_lines:
basket.add_product(self.verified_product) basket.add_product(self.verified_product)
basket.add_product(self.honor_product) basket.add_product(self.honor_product)
elif free: elif free:
......
/**
* Basket page scripts.
**/
define([
'jquery'
],
function ($
) {
'use strict';
var onReady = function() {
var el = $('#receipt-container'),
order_id = el.data('order-id'),
fire_tracking_events = el.data('fire-tracking-events'),
total_amount = el.data('total-amount'),
currency = el.data('currency');
if(order_id && fire_tracking_events){
trackPurchase(order_id, total_amount, currency);
}
},
trackPurchase = function(order_id, total_amount, currency) {
window.analytics.track('Completed Purchase', {
orderId: order_id,
total: total_amount,
currency: currency
});
};
$(document).ready(onReady);
return {
onReady: onReady,
};
}
);
define([
'pages/receipt_page',
'utils/analytics_utils'
],
function (ReceiptPage,
AnalyticsUtils
) {
'use strict';
describe('Receipt Page', function () {
beforeEach(function () {
$('<script type="text/javascript">var initModelData = {};</script>').appendTo('body');
$(
'<div id="receipt-container" data-fire-tracking-events="true" data-order-id="ORDER_ID"></div>'
).appendTo('body');
AnalyticsUtils.analyticsSetUp();
/* jshint ignore:start */
// jscs:disable
window.analytics = window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="3.0.1";}
// jscs:enable
/* jshint ignore:end */
});
describe('onReady', function () {
it('should trigger track purchase', function () {
spyOn(window.analytics, 'track');
ReceiptPage.onReady();
expect(window.analytics.track).toHaveBeenCalled();
});
});
});
}
);
...@@ -42,22 +42,3 @@ a { ...@@ -42,22 +42,3 @@ a {
background: #fcfcfc; background: #fcfcfc;
box-shadow: 0 1px 2px 1px rgba(167, 164, 164, 0.25); box-shadow: 0 1px 2px 1px rgba(167, 164, 164, 0.25);
} }
.depth--3 {
background: #34383a;
text-align: center;
.copy {
color: #e7e6e6;
@include container-fixed;
@media (min-width: $screen-sm-min) {
width: $container-sm;
}
@media (min-width: $screen-md-min) {
width: $container-md;
}
@media (min-width: $screen-lg-min) {
width: $container-lg;
}
}
}
...@@ -35,5 +35,6 @@ ...@@ -35,5 +35,6 @@
@import 'views/coupon_admin'; @import 'views/coupon_admin';
@import 'views/coupon_offer'; @import 'views/coupon_offer';
@import 'views/error'; @import 'views/error';
@import 'views/receipt';
@import "default"; @import "default";
.receipt {
background-color: #ffffff;
font-family: $font-family-sans-serif;
line-height: 1em;
.action-primary {
background: palette(primary, base) none repeat scroll 0 0;
border: 0 none;
box-shadow: 0 2px 1px 0 palette(primary, dark);
color: #fff;
text-decoration: none;
&:hover, &:focus {
background: palette(primary, accent) none repeat scroll 0 0;
color: #fff;
text-decoration: none;
}
}
.content-container {
padding: 15px 30px 40px;
}
.copy {
> div {
margin-bottom: 15px;
line-height: 22px;
}
.billing-address {
margin-top: 35px;
margin-bottom: 35px;
}
}
#error-container {
padding: 15px;
background: #fbf4f7 none repeat scroll 0 0;
border-bottom: 4px solid #b20610;
.error-message {
background: inherit;
.title {
margin-bottom: 5px;
font-weight: 600;
}
}
}
.info-item {
overflow: hidden;
}
.payment-info {
margin-bottom: spacing-vertical(small);
}
.is-ready {
background: #e8f6fc none repeat scroll 0 0;
}
.nav-link {
border-bottom: none;
text-decoration: none !important;
&:active, &:hover, &:focus {
border-bottom: 1px dotted #0079bc;
}
}
.nav-wizard {
padding: 15px 20px;
margin: 0;
overflow: hidden;
border-top: 4px solid rgb(255, 192, 31);
box-shadow: 0 1px 1px 2px rgba(0, 0, 0, 0.2);
.header {
margin: 0;
padding: 10px 0;
color: #434242;
font-size: 18px;
font-weight: bold;
}
.message {
color: #434242;
font-weight: 500;
line-height: 1.4em;
}
#verify_now {
margin-bottom: 10px;
}
#verify_choices {
margin-bottom: 10px;
padding-top: 10px;
text-align: center;
}
#verify_now_button {
display: block;
width: 100%;
margin: 0 auto;
padding: 13px 35px;
text-align: center;
&:hover {
text-decoration: none;
}
}
}
.confirm-message {
font-size: 18px;
}
h2 {
margin-bottom: 10px;
color: rgb(0, 121, 188);
}
p {
margin: 0;
}
.thank-you {
font-size: 38px;
}
.price {
padding-right: 0;
font-size: 16px;
font-weight: bold;
text-align: right;
}
#dashboard-link {
margin: 20px;
text-align: center;
.dashboard-link {
font-size: 18px;
}
}
.dashboard-link {
line-height: 20px;
font-size: 13px;
}
.order-summary {
dt {
font-weight: 500;
}
dd {
margin-bottom: 20px;
color: rgb(29, 29, 29);
font-size: 18px;
}
}
.order-headers {
color: rgb(0, 121, 188);
font-size: 22px;
}
.order-line-data {
color: rgb(29, 29, 29);
font-size: 20px;
> td {
padding-bottom: 1.5em;
padding-top: 1.25em;
line-height: 1.5em !important;
}
}
p, address {
color: #6f7074;
}
/* Styling for the provider area; uses edX Pattern Library utils */
.report-receipt-provider {
@include clearfix;
margin: 0 0 20px 0;
padding: 15px 20px;
border: 1px solid rgb(221,221,221);
border-radius: 3px;
box-shadow: 0 1px 2px 1px rgba(0,0,0,0.1);
background: rgb(255,255,255);
.provider-wrapper {
@include float(left);
padding: 10px;
.provider-info {
margin-bottom: 20px;
font-weight: 600;
}
}
.provider-buttons-logos {
padding: 10px;
@include text-align(center);
.provider-logo img {
max-width: 160px;
margin-bottom: 10px;
}
}
}
.summary {
td:first-child {
padding: 8px 0 0 0;
}
}
.summary-description {
font-weight: bold;
}
.table {
dl {
margin: 0;
}
.header, .order-line-data {
dt, dd {
padding: 10px 10px 5px 10px;
&:not(:last-child) {
float: left;
border-right: 2px solid rgb(235, 235, 235);
}
&:last-child {
display: inline-block;
width: 15%;
}
}
dt:last-child {
text-align: center;
}
dd {
height: 100px;
}
}
.header {
background-color: rgb(249, 249, 249);
dt {
border-bottom: 2px solid rgb(235, 235, 235);
font-size: 16px;
&:first-child {
width: 10%;
}
&:nth-child(2) {
width: 75%;
}
}
}
.order-line-data {
&:not(:last-child) {
border-bottom: 2px solid rgb(235, 235, 235);
}
&:last-child {
border-bottom: 4px solid;
}
dd {
font-size: 16px;
&:nth-child(2) {
width: 10%;
}
&:nth-child(4) {
width: 75%;
}
&.line-price {
padding-right: 5%;
}
}
}
.order-total {
margin-left: 60%;
padding: 20px 0;
&:not(:last-child) {
border-bottom: 2px solid rgb(235, 235, 235);
}
.description {
width: 62.5%;
font-weight: bold;
p {
padding-top: 5px;
font-size: 15px;
font-weight: normal;
}
}
.description, .price {
display: inline-block;
}
.price {
float: right;
margin-right: 12%;
color: black;
}
}
}
.wrapper-content-main {
margin-bottom: 20px;
}
}
/* Small devices (tablets, 992px and lower) */
@media (max-width: $screen-sm-max) {
.receipt {
.sr {
position: static;
width: auto;
height: auto;
font-size: 16px;
}
.thank-you {
text-align: center;
}
.order-summary {
border-top: 2px solid rgb(235, 235, 235);
border-bottom: 2px solid rgb(235, 235, 235);
padding: 10px 0;
background-color: rgb(249, 249, 249);
dt, dd {
padding-left: 10px;
font-size: 16px;
}
dt {
float: left;
color: black;
font-weight: bold;
}
dd {
display: flex;
}
}
.table {
border-top: 4px solid rgb(235, 235, 235);
.header {
display: none;
dt {
border-bottom: none;
}
}
.order-line-data {
padding-top: 10px;
&:not(:last-child) {
border-bottom: none;
}
&:last-child {
border-bottom: 2px solid;
}
dt, dd {
&:not(:last-child) {
float: none;
border-right: none;
}
}
dd {
display: inline-block;
height: auto;
&:nth-child(2), &:nth-child(4) {
width: auto;
}
&.course-description {
padding: 0 10px;
}
&.line-price {
font-weight: normal;
}
}
dt {
display: inline;
float: left;
}
.quantity, .line-price {
display: inline;
}
.course-description {
display: block;
width: auto;
}
}
.order-total {
margin-left: 0;
padding: 20px 10px;
.price {
margin-right: 0;
}
}
}
}
}
{% extends 'edx/base.html' %}
{% load core_extras %}
{% load currency_filters %}
{% load i18n %}
{% load offer_tags %}
{% load staticfiles %}
{% block title %}
{% blocktrans with order_number=order.number %}
Receipt for {{ order_number }}
{% endblocktrans %}
{% endblock title %}
{% block javascript %}
<script src="{% static 'js/pages/receipt_page.js' %}"></script>
{% endblock javascript %}
{% block navbar %}
{% include 'edx/partials/_student_navbar.html' %}
{% endblock navbar %}
{% block content %}
<div id="receipt-container"
class="receipt container content-container"
data-currency="{{ order.currency }}"
data-fire-tracking-events="{{ fire_tracking_events|yesno:"true,false" }}"
data-order-id="{{ order.number }}"
data-total-amount="{{ order.total_incl_tax }}">
<h2 class="thank-you">{% trans "Thank you for your order!" %}</h2>
<div class="list-info">
<div class="info-item payment-info row">
<div class="copy col-md-8">
<div class="confirm-message">
{% captureas link_start %}
<a href="mailto:{{ order.user.email }}">
{% endcaptureas %}
{% blocktrans with email=order.user.email link_end="</a>" %}
Your order is complete. If you need a receipt, you can print this page.
You will also receive a confirmation message with this information at
{{ link_start }}{{ email }}{{ link_end }}.
{% endblocktrans %}
</div>
{% if order.billing_address %}
<address class="billing-address">
{% for field in order.billing_address.active_address_fields %}
{{ field }}<br/>
{% endfor %}
</address>
{% endif %}
</div>
<div class="order-headers order-summary col-md-4">
<dl>
<dt>{% trans "Order Number:" %}</dt>
<dd>{{ order.number }}</dd>
{% if payment_method %}
<dt>{% trans "Payment Method:" %}</dt>
<dd>{{ payment_method }}</dd>
{% endif %}
<dt>{% trans "Order Date:" %}</dt>
<dd>{{ order.date_placed|date:"E d, Y" }}</dd>
</dl>
</div>
</div>
<h2>{% trans "Order Information" %}</h2>
<div class="table">
<dl class="order-lines">
<div class="header">
<dt aria-hidden="true">{% trans "Quantity" %}</dt>
<dt aria-hidden="true">{% trans "Description" %}</dt>
<dt aria-hidden="true">{% trans "Item Price" %}</dt>
</div>
{% for line in order.lines.all %}
<div class="order-line-data">
<dt class="quantity sr">{% trans "Quantity:" %}</dt>
<dd class="quantity">{{ line.quantity }}</dd>
<dt class="course-description sr">{% trans "Description:" %}</dt>
<dd class="course-description">
<span>{{ line.description }}</span>
<p>{{ line.product.course.id|course_organization }}</p>
</dd>
<dt class="line-price sr">{% trans "Item Price:" %}</dt>
<dd class="line-price price">{{ line.line_price_before_discounts_incl_tax|currency:order.currency }}</dd>
</div>
{% endfor %}
</dl>
<div class="order-total">
<div class="description">{% trans "Subtotal:" %}</div>
<div class="price">{{ order.total_before_discounts_incl_tax|currency:order.currency }}</div>
</div>
{% if order.total_discount_incl_tax %}
{% for voucher in order.basket.vouchers.all %}
<div class="order-total">
<div class="description">
<span>{% trans "Coupon applied:" %}</span>
<p>
{% blocktrans with voucher_code=voucher.code voucher_discount_amount=voucher.benefit|benefit_discount %}
{{ voucher_code }} {{ voucher_discount_amount }} off
{% endblocktrans %}
</p>
</div>
<div class="price">
-{{ order.total_discount_incl_tax|currency:order.currency }}
</div>
</div>
{% endfor %}
{% endif %}
<div class="order-total">
<div class="description">{% trans "Total:" %}</div>
<div class="price">{{ order.total_incl_tax|currency:order.currency }}</div>
</div>
</div>
{% if display_credit_messaging %}
{% captureas link_start %}
<a href="{{ lms_dashboard_url }}">
{% endcaptureas %}
<div class="nav-wizard row">
<p class="header">{% trans "Get Your Course Credit" %}</p>
<p class="message">
{% blocktrans with link_end="</a>" %}
To receive academic credit for this course, you must apply for credit at the organization that offers the credit.
You can find a link to the organization’s website on your {{ link_start }}dashboard{{ link_end }}, next to the course name.
{% endblocktrans %}
</p>
</div>
{% endif %}
</div>
{% if verified_course_id and not user_verified %}
<div class="nav-wizard row">
{% include 'oscar/checkout/_verification_data.html' with course_id=verified_course_id %}
</div>
{% else %}
<div id="dashboard-link">
<a class="dashboard-link nav-link" href="{{ lms_dashboard_url }}">
{% trans "Go to Dashboard" %}
</a>
</div>
{% endif %}
</div>
</div>
{% endblock content %}
{% extends 'edx/base.html' %}
{% load core_extras %}
{% load i18n %}
{% block title %}
{% trans "Order Not Found" %}
{% endblock title %}
{% block navbar %}
{% include 'edx/partials/_student_navbar.html' %}
{% endblock navbar %}
{% block content %}
<div class="receipt">
<div id="error-container">
<div class="error-message container">
<h3 class="title">
<span class="sr">{% blocktrans %} {{ error_summary }} {% endblocktrans %}</span>
{{ error_summary }}
</h3>
<div class="copy">
<p>{% trans "The specified order could not be located. Please ensure that the URL is correct, and try again." %}</p>
<br/>
</div>
<div class="msg">
<p>
{% captureas link_start %}
<a href="{{ order_history_url }}">
{% endcaptureas %}
{% blocktrans with link_end="</a>" %}
You may also view your previous orders on the {{ link_start }}Account Settings{{ link_end }}
page.
{% endblocktrans %}
</p>
</div>
</div>
</div>
</div>
{% endblock content %}
{% load i18n %}
<div class="col-md-9">
<p class="header">{% trans "Verify Your Identity" %}</p>
<p class="message">
{% blocktrans %}
To receive a verified certificate, you have to verify your identity using your <strong>webcam</strong> and
an <strong>official government-issued photo identification</strong> before the verification deadline for this
course.
{% endblocktrans %}
</p>
</div>
<div id="verify_choices" class="right col-md-3">
<div id="verify_now">
<a id="verify_now_button"
class="next action-primary"
data-track-category="verification"
data-track-event="edx.bi.user.verification.immediate"
data-track-type="click"
href="{{ verify_url }}{{ course_id }}/">
{% trans "Verify Now" %}
</a>
</div>
<a id="verify_later_button"
class="dashboard-link"
data-track-category="verification"
data-track-event="edx.bi.user.verification.immediate"
data-track-type="click"
href="{{ lms_dashboard_url }}">
{% trans "Go to my dashboard and verify later" %}
</a>
</div>
...@@ -22,6 +22,5 @@ ...@@ -22,6 +22,5 @@
{{ payment_support_email }}{{ end_link }}. {{ payment_support_email }}{{ end_link }}.
{% endblocktrans %} {% endblocktrans %}
{% endwith %}</p> {% endwith %}</p>
</div> </div>
{% endblock content %} {% endblock content %}
...@@ -284,6 +284,12 @@ class SiteMixin(object): ...@@ -284,6 +284,12 @@ class SiteMixin(object):
return token return token
def toggle_ecommerce_receipt_page(self, enable_otto_receipt_page):
""" Enables Ecommerce Receipt Page. """
site_configuration = self.site.siteconfiguration
site_configuration.enable_otto_receipt_page = enable_otto_receipt_page
site_configuration.save()
class TestServerUrlMixin(object): class TestServerUrlMixin(object):
def get_full_url(self, path, site=None): def get_full_url(self, path, site=None):
......
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