Commit 47d7812b by Marko Jevtić Committed by GitHub

Merge pull request #870 from edx/mjevtic/SOL-1966

[SOL-1966] Performance optimization - Offer Landing Page
parents 650e2508 b0b7a8b9
......@@ -3,6 +3,7 @@ import json
import httpretty
from django.conf import settings
from django.core.cache import cache
from django.test import RequestFactory
from oscar.test import factories
......@@ -18,12 +19,14 @@ class CourseCatalogMockMixin(object):
def setUp(self):
super(CourseCatalogMockMixin, self).setUp()
cache.clear()
def mock_dynamic_catalog_course_runs_api(self, course_run=None, query=None, course_run_info=None):
""" Helper function to register a dynamic course catalog API endpoint for the course run information. """
if not course_run_info:
course_run_info = {
'count': 1,
'next': 'path/to/next/page',
'results': [{
'key': course_run.id,
'title': course_run.name,
......
""" Coupon related utility functions. """
import hashlib
from django.conf import settings
from django.core.cache import cache
from oscar.core.loading import get_model
from ecommerce.core.constants import DEFAULT_CATALOG_PAGE_SIZE
......@@ -6,6 +10,32 @@ from ecommerce.core.constants import DEFAULT_CATALOG_PAGE_SIZE
Product = get_model('catalogue', 'Product')
def get_range_catalog_query_results(limit, query, site, offset=None):
"""
Get catalog query results
Arguments:
limit (int): Number of results per page
query (str): ElasticSearch Query
site (Site): Site object containing Site Configuration data
offset (int): Page offset
Returns:
dict: Query seach results received from Course Catalog API
"""
cache_key = 'course_runs_{}_{}_{}'.format(query, limit, offset)
cache_hash = hashlib.md5(cache_key).hexdigest()
response = cache.get(cache_hash)
if not response:
response = site.siteconfiguration.course_catalog_api_client.course_runs.get(
limit=limit,
offset=offset,
q=query,
)
cache.set(cache_hash, response, settings.COURSES_API_CACHE_TIMEOUT)
return response
def get_seats_from_query(site, query, seat_types):
"""
Retrieve seats from a course catalog query and matching seat types.
......@@ -18,14 +48,16 @@ def get_seats_from_query(site, query, seat_types):
Returns:
List of seat products retrieved from the course catalog query.
"""
response = site.siteconfiguration.course_catalog_api_client.course_runs.get(q=query,
page_size=DEFAULT_CATALOG_PAGE_SIZE,
limit=DEFAULT_CATALOG_PAGE_SIZE)
results = get_range_catalog_query_results(
limit=DEFAULT_CATALOG_PAGE_SIZE,
query=query,
site=site
)['results']
query_products = []
for result in response['results']:
for course in results:
try:
product = Product.objects.get(
course_id=result['key'],
course_id=course['key'],
attributes__name='certificate_type',
attribute_values__value_text__in=seat_types.split(',')
)
......
......@@ -76,6 +76,7 @@ class VoucherViewSetTests(CourseCatalogMockMixin, CourseCatalogTestMixin, TestCa
course2, seat2 = self.create_course_and_seat()
course_run_info = {
'count': 2,
'next': 'path/to/the/next/page',
'results': [{
'key': course1.id,
'title': course1.name,
......@@ -103,11 +104,11 @@ class VoucherViewSetTests(CourseCatalogMockMixin, CourseCatalogTestMixin, TestCa
request = factory.get('/?code={}&page_size=6'.format(voucher.code))
request.site = self.site
request.strategy = DefaultStrategy()
offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher)
offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher)['results']
self.assertEqual(len(offers), 2)
products[1].expires = pytz.utc.localize(datetime.datetime.min)
offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher)
offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher)['results']
self.assertEqual(len(offers), 1)
......@@ -235,7 +236,7 @@ class VoucherViewOffersEndpointTests(
request = self.prepare_offers_listing_request(voucher.code)
with mock.patch(method, mock.Mock(return_value=return_value)):
offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher)
offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher)['results']
self.assertEqual(len(offers), 0)
@mock_course_catalog_api_client
......@@ -263,7 +264,7 @@ class VoucherViewOffersEndpointTests(
benefit = voucher.offers.first().benefit
request = self.prepare_offers_listing_request(voucher.code)
self.mock_course_api_response(course=course)
offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher)
offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher)['results']
first_offer = offers[0]
self.assertEqual(len(offers), 1)
......@@ -294,7 +295,7 @@ class VoucherViewOffersEndpointTests(
voucher, products = get_voucher_and_products_from_code(voucher.code)
benefit = voucher.offers.first().benefit
request = self.prepare_offers_listing_request(voucher.code)
offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher)
offers = VoucherViewSet().get_offers(products=products, request=request, voucher=voucher)['results']
first_offer = offers[0]
self.assertEqual(len(offers), 1)
......
......@@ -11,6 +11,7 @@ from rest_framework_extensions.mixins import NestedViewSetMixin
from slumber.exceptions import SlumberBaseException
from ecommerce.core.constants import DEFAULT_CATALOG_PAGE_SIZE
from ecommerce.coupons.utils import get_range_catalog_query_results
from ecommerce.courses.models import Course
from ecommerce.extensions.api import serializers
......@@ -43,9 +44,11 @@ class CatalogViewSet(NestedViewSetMixin, ReadOnlyModelViewSet):
if query and seat_types:
seat_types = seat_types.split(',')
try:
client = request.site.siteconfiguration.course_catalog_api_client
results = client.course_runs.get(q=query, page_size=DEFAULT_CATALOG_PAGE_SIZE,
limit=DEFAULT_CATALOG_PAGE_SIZE)['results']
results = get_range_catalog_query_results(
limit=DEFAULT_CATALOG_PAGE_SIZE,
query=query,
site=request.site
)['results']
course_ids = [result['key'] for result in results]
courses = serializers.CourseSerializer(
Course.objects.filter(id__in=course_ids),
......
"""HTTP endpoints for interacting with vouchers."""
import hashlib
import logging
from urlparse import urlparse
from django.conf import settings
from django.core.cache import cache
from django.http import Http404
from django.shortcuts import get_object_or_404
import django_filters
......@@ -19,6 +17,7 @@ from ecommerce.core.constants import DEFAULT_CATALOG_PAGE_SIZE
from ecommerce.core.url_utils import get_lms_url
from ecommerce.courses.models import Course
from ecommerce.courses.utils import get_course_info_from_lms
from ecommerce.coupons.utils import get_range_catalog_query_results
from ecommerce.coupons.views import get_voucher_and_products_from_code
from ecommerce.extensions.api import exceptions, serializers
from ecommerce.extensions.api.permissions import IsOffersOrIsAuthenticatedAndStaff
......@@ -75,27 +74,25 @@ class VoucherViewSet(NonDestroyableModelViewSet):
logger.error('No product(s) are associated with this code.')
return Response(status=status.HTTP_400_BAD_REQUEST)
query = voucher.offers.first().benefit.range.catalog_query
if query:
cache_key = 'voucher_offers_{}'.format(query)
else:
cache_key = 'voucher_offers_{}'.format(voucher.id)
cache_hash = hashlib.md5(cache_key).hexdigest()
offers = cache.get(cache_hash)
if not offers:
try:
offers = self.get_offers(products, request, voucher)
except (ConnectionError, SlumberBaseException, Timeout):
logger.error('Could not get course information.')
return Response(status=status.HTTP_400_BAD_REQUEST)
except Http404:
logger.error('Could not get information for product %s.', products[0].title)
return Response(status=status.HTTP_404_NOT_FOUND)
cache.set(cache_hash, offers, settings.COURSES_API_CACHE_TIMEOUT)
page = self.paginate_queryset(offers)
return self.get_paginated_response(page)
try:
offers_data = self.get_offers(products, request, voucher)
except (ConnectionError, SlumberBaseException, Timeout):
logger.error('Could not get course information.')
return Response(status=status.HTTP_400_BAD_REQUEST)
except Http404:
logger.error('Could not get information for product %s.', products[0].title)
return Response(status=status.HTTP_404_NOT_FOUND)
next_page = offers_data['next']
if next_page:
next_page_query = urlparse(next_page).query
offers_data['next'] = '{path}?{query}&code={code}'.format(
code=code,
path=request.path,
query=next_page_query,
)
return Response(data=offers_data)
def get_offers(self, products, request, voucher):
"""
......@@ -105,18 +102,21 @@ class VoucherViewSet(NonDestroyableModelViewSet):
request (HttpRequest): Request data
voucher (Voucher): Oscar Voucher for which the offers are returned
Returns:
List: List of course offers where each offer is represented by a dictionary
dict: Dictionary containing a link to the next page of Course Discovery results and
a List of course offers where each offer is represented as a dictionary
"""
benefit = voucher.offers.first().benefit
catalog_query = benefit.range.catalog_query
next_page = None
offers = []
if catalog_query:
query_results = request.site.siteconfiguration.course_catalog_api_client.course_runs.get(
q=catalog_query,
page_size=DEFAULT_CATALOG_PAGE_SIZE,
limit=DEFAULT_CATALOG_PAGE_SIZE
)['results']
response = get_range_catalog_query_results(
limit=request.GET.get('limit', DEFAULT_CATALOG_PAGE_SIZE),
offset=request.GET.get('offset'),
query=catalog_query,
site=request.site
)
next_page = response['next']
course_ids = [product.course_id for product in products]
courses = Course.objects.filter(id__in=course_ids)
stock_records = StockRecord.objects.filter(product__in=products)
......@@ -129,7 +129,10 @@ class VoucherViewSet(NonDestroyableModelViewSet):
logger.info('%s is unavailable to buy. Omitting it from the results.', product)
continue
course_id = product.course_id
course_catalog_data = next((result for result in query_results if result['key'] == course_id), None)
course_catalog_data = next(
(result for result in response['results'] if result['key'] == course_id),
None
)
try:
stock_record = stock_records.get(product__id=product.id)
......@@ -170,7 +173,7 @@ class VoucherViewSet(NonDestroyableModelViewSet):
stock_record=stock_record,
voucher=voucher
))
return offers
return {'next': next_page, 'results': offers}
def get_course_offer_data(self, benefit, course, course_info, is_verified, stock_record, voucher):
"""
......
define([
'backbone',
'models/offer_model',
'underscore.string'
'collections/paginated_collection',
'models/offer_model'
],
function (Backbone,
OfferModel,
_s) {
PaginatedCollection,
OfferModel) {
'use strict';
return Backbone.Collection.extend({
return PaginatedCollection.extend({
model: OfferModel,
baseUrl: '/api/v2/vouchers/offers/',
url: '/api/v2/vouchers/offers/',
initialize: function(options) {
if (options) {
this.code = options.code;
}
initialize: function() {
this.page = 1;
this.perPage = 6;
this.updateLimits();
this.on('update', this.updateNumberOfPages);
},
parse: function(response) {
if (response.page) {
this.page = response.page;
}
this.total = response.count;
this.prev = response.previous;
this.next = response.next;
return response.results;
updateNumberOfPages: function() {
this.numberOfPages = Math.ceil(this.length / this.perPage);
},
url: function() {
return _s.sprintf('%s?%s',
this.baseUrl,
$.param({
code: this.code,
page: this.page,
page_size: this.perPage
})
);
},
pageInfo: function() {
var info = {
total: this.total,
page: this.page,
perPage: this.perPage,
pages: this.numberOfPages(),
prev: false,
next: false
},
max = Math.min(this.total, this.page * this.perPage);
if (this.total === this.pages * this.perPage) {
max = this.total;
}
info.range = [(this.page - 1) * this.perPage + 1, max];
if (this.page > 1) {
info.prev = this.page - 1;
}
if (this.page < info.pages) {
info.next = this.page + 1;
}
return info;
updateLimits: function() {
this.lowerLimit = (this.page - 1) * this.perPage;
this.upperLimit = this.page * this.perPage;
},
numberOfPages: function() {
return Math.ceil(this.total / this.perPage);
goToPage: function(pageNumber) {
this.page = pageNumber;
this.updateLimits();
return this.slice(this.lowerLimit, this.upperLimit);
},
nextPage: function() {
if (!this.pageInfo().next) {
if (this.onLastPage()) {
return false;
} else {
this.page = this.page + 1;
return this.fetch();
return this.goToPage(this.page + 1);
}
},
},
previousPage: function() {
if (!this.pageInfo().prev) {
if (this.onFirstPage()) {
return false;
} else {
this.page = this.page - 1;
return this.fetch();
return this.goToPage(this.page - 1);
}
},
goToPage: function(ev) {
this.page = parseInt($(ev.target).text());
return this.fetch();
}
onFirstPage: function() {
return this.page === 1;
},
onLastPage: function() {
return this.page === this.numberOfPages;
}
});
}
);
......@@ -11,7 +11,6 @@ define([
this.url = response.next;
this.fetch({remove: false});
}
return response.results;
}
});
......
......@@ -14,10 +14,10 @@ define([
title: gettext('Redeem'),
initialize: function(options) {
this.collection = new OfferCollection({code: options.code});
this.collection = new OfferCollection();
this.view = new OfferView({code: options.code, collection: this.collection});
this.listenTo(this.collection, 'update', this.refresh);
this.collection.fetch();
this.render();
this.collection.fetch({remove: false, data: {code: options.code, limit: 50}});
}
});
}
......
......@@ -32,19 +32,19 @@ define([
});
it('should fetch the next page of results', function() {
spyOn(collection, 'fetch');
spyOn(collection, 'goToPage');
collection.page = 1;
collection.total = 8;
collection.perPage = 4;
collection.nextPage();
expect(collection.fetch).toHaveBeenCalled();
expect(collection.goToPage).toHaveBeenCalledWith(2);
});
it('should not fetch the next page of results', function() {
spyOn(collection, 'fetch');
collection.page = 2;
collection.total = 8;
collection.numberOfPages = 2;
collection.perPage = 4;
response = collection.nextPage();
......@@ -52,13 +52,13 @@ define([
});
it('should fetch the previous page of results', function() {
spyOn(collection, 'fetch');
spyOn(collection, 'goToPage');
collection.page = 2;
collection.total = 8;
collection.numberOfPages = 8;
collection.perPage = 4;
collection.previousPage();
expect(collection.fetch).toHaveBeenCalled();
expect(collection.goToPage).toHaveBeenCalledWith(1);
});
it('should not fetch the previous page of results', function() {
......@@ -72,33 +72,12 @@ define([
});
it('should fetch the page that is selected', function() {
var ev = $.Event('click');
ev.target = '<div>1</div>';
spyOn(collection, 'fetch');
collection.goToPage(ev);
expect(collection.page).toBe(1);
expect(collection.fetch).toHaveBeenCalled();
});
spyOn(collection, 'updateLimits');
it('should set page', function() {
collection.parse(response);
collection.goToPage(1);
expect(collection.page).toBe(1);
expect(collection.updateLimits).toHaveBeenCalled();
});
it('should set code', function() {
collection = new OfferCollection({code: 'abcd'});
expect(collection.code).toBe('abcd');
});
it('should return url with parameters set', function() {
collection.code = 'abcd';
collection.page = 1;
collection.perPage = 2;
expect(collection.url()).toBe('/api/v2/vouchers/offers/?code=abcd&page=1&page_size=2');
});
});
}
);
......@@ -35,7 +35,8 @@ define([
title: 'edX Demonstration Course',
course_start_date: '2013-02-05T05:00:00Z',
id: 'course-v1:edX+DemoX+Demo_Course',
voucher_end_date: '2016-07-29T00:00:00Z'
voucher_end_date: '2016-07-29T00:00:00Z',
contains_verified: true,
}),
course2 = new OfferModel({
benefit: {
......@@ -56,18 +57,26 @@ define([
title: 'edX Demonstration Course',
course_start_date: '2013-02-05T05:00:00Z',
id: 'course-v1:edX+DemoX+Demo_Courseewewe',
voucher_end_date: '2016-07-29T00:00:00Z'
voucher_end_date: '2016-07-29T00:00:00Z',
contains_verified: true,
});
beforeEach(function() {
$('body').append('<div class="verified-info"></div>');
code = 'ABCDE';
collection = new OfferCollection(null, {code: code});
collection.add([ course, course2 ]);
view = new OfferView({code: code, collection: collection});
view = new OfferView({code: code, collection: collection}).render();
});
it('should show the verified certificate info', function() {
expect($('.verified-info').hasClass('hidden')).toBeFalsy();
});
it('should hide the verified certificate info', function() {
expect(view.$('.verified-info:hidden')).toBeTruthy();
view.collection.at(0).set('contains_verified', false);
view.showVerifiedCertificate();
expect($('.verified-info').hasClass('hidden')).toBeTruthy();
});
it('should call functions when formatValues called with course', function() {
......@@ -81,10 +90,8 @@ define([
});
it('should call functions when refreshData called', function() {
spyOn(view, 'showVerifiedCertificate');
spyOn(_, 'each');
view.refreshData();
expect(view.showVerifiedCertificate).toHaveBeenCalled();
expect(_.each).toHaveBeenCalledWith(view.collection.models, view.formatValues, view);
});
......@@ -130,31 +137,17 @@ define([
it('should fetch the page that is selected', function() {
var ev = $.Event('click');
ev.target = '<div>1</div>';
spyOn(view.collection, 'fetch');
spyOn(view.collection, 'goToPage');
view.goToPage(ev);
expect(view.collection.page).toBe(1);
expect(view.collection.fetch).toHaveBeenCalled();
});
it('should fetch the next page of results', function() {
spyOn(view.collection, 'fetch');
view.collection.page = 1;
view.collection.total = 8;
view.collection.perPage = 4;
view.next();
expect(view.collection.fetch).toHaveBeenCalled();
expect(view.collection.goToPage).toHaveBeenCalled();
});
it('should fetch the previous page of results', function() {
spyOn(view.collection, 'fetch');
view.collection.page = 2;
view.collection.total = 8;
view.collection.perPage = 4;
spyOn(view.collection, 'previousPage');
view.previous();
expect(view.collection.fetch).toHaveBeenCalled();
expect(view.collection.previousPage).toHaveBeenCalled();
});
it('should create list item', function() {
......@@ -168,7 +161,7 @@ define([
it('should create ellipsis item', function() {
var value = view.createEllipsisItem(),
string = '<li class="page-item disabled">' +
'<button aria-label="Ellipsis" class="page-number page-link"><span>' +
'<button aria-label="Ellipsis" class="page-number page-link disabled"><span>' +
'&hellip;</span></button</li>';
expect(value).toBe(string);
});
......@@ -197,17 +190,16 @@ define([
spyOn(view, 'createNextItem');
spyOn(view, 'createListItem');
collection.total = 10;
collection.numberOfPages = 10;
collection.perPage = 1;
for (var i=1; i<=collection.total; i++) {
for (var i=1; i<=collection.numberOfPages; i++) {
collection.page = i;
collection.pageInfo();
view.renderPagination();
expect(view.createPreviousItem).toHaveBeenCalled();
expect(view.createNextItem).toHaveBeenCalled();
if (collection.page - 4 >= 1 && collection.page + 4 <= collection.total) {
if (collection.page - 4 >= 1 && collection.page + 4 <= collection.numberOfPages) {
expect(view.createEllipsisItem.calls.count()).toBe(ellipsisSpyCounter+1);
ellipsisSpyCounter += 2;
}else {
......@@ -222,24 +214,41 @@ define([
spyOn(view, 'createPreviousItem');
spyOn(view, 'createNextItem');
collection.total = 2;
collection.perPage = 1;
collection.page = 1;
collection.next = 'some/link';
collection.prev = null;
collection.numberOfPages = 5;
view.renderPagination();
expect(view.createPreviousItem).toHaveBeenCalledWith(collection.prev);
expect(view.createNextItem).toHaveBeenCalledWith(collection.next);
expect(view.createPreviousItem).toHaveBeenCalledWith(true);
expect(view.createNextItem).toHaveBeenCalledWith(false);
collection.next = null;
collection.prev = 'some/link';
collection.page = 5;
collection.numberOfPages = 5;
view.renderPagination();
expect(view.createPreviousItem).toHaveBeenCalledWith(collection.prev);
expect(view.createNextItem).toHaveBeenCalledWith(collection.next);
expect(view.createPreviousItem).toHaveBeenCalledWith(false);
expect(view.createNextItem).toHaveBeenCalledWith(true);
});
it('should get next page from the collection', function() {
var next_collection_page = {'1': 1, '2': 2};
spyOn(view.collection, 'nextPage').and.returnValue(next_collection_page);
spyOn(view, 'changePage');
view.next();
expect(view.collection.nextPage).toHaveBeenCalled();
expect(view.page).toEqual(next_collection_page);
expect(view.changePage).toHaveBeenCalled();
});
it('should get previous page from the collection', function() {
var previous_collection_page = {'1': 1, '2': 2};
spyOn(view.collection, 'previousPage').and.returnValue(previous_collection_page);
spyOn(view, 'changePage');
view.previous();
expect(view.collection.previousPage).toHaveBeenCalled();
expect(view.page).toEqual(previous_collection_page);
expect(view.changePage).toHaveBeenCalled();
});
});
}
);
\ No newline at end of file
);
......@@ -4,79 +4,79 @@
<h3 class="number-of-products-header col-xs-12"><%- gettext('Select one of the following courses.') %></h3>
<% } %>
<% if (courses && courses[0].attributes.stockrecords) { %>
<% if(pageInfo.pages > 1) { %>
<% if (courses) { %>
<% if(courses.numberOfPages > 1) { %>
<div class="pagination-block col-xs-12">
<nav class="pull-right hidden-xs">
<nav class="pull-right">
<ul class="pagination">
</ul>
</nav>
<h4 class="pagination-range pull-right">Showing <%= pageInfo.range[0] %> - <%= pageInfo.range[1] %> of [<%= pageInfo.total %>]</h4>
<h4 class="pagination-range pull-right hidden-xs">Showing <%= (courses.lowerLimit + 1) %> - <%= courses.upperLimit %> of [<%= courses.length %>]</h4>
</div>
<% } %>
<% _.each(courses, function(course) { %>
<div class="discount-multiple-courses col-xs-12 <% if (courses.length > 1) { %>col-lg-6<% } %>">
<div class="box-shadow col-xs-12">
<div class="col-sm-5">
<div class="image-container">
<div class="discount-percentage"><p><%= course.attributes.benefit_value %>
<span><%- gettext('off') %></span></p>
</div>
<img class="img-responsive" src="<%= course.attributes.image_url %>"
alt="<%= course.attributes.title %>"/>
<% _.each(page, function(course) { %>
<div class="discount-multiple-courses col-xs-12 <% if (courses.length > 1) { %>col-lg-6<% } %>">
<div class="box-shadow col-xs-12">
<div class="col-sm-5">
<div class="image-container">
<div class="discount-percentage"><p><%= course.attributes.benefit_value %>
<span><%- gettext('off') %></span></p>
</div>
<img class="img-responsive" src="<%= course.attributes.image_url %>"
alt="<%= course.attributes.title %>"/>
<div class="voucher-valid-until">
<p><%= course.attributes.voucher_end_date_text %></p>
<div class="voucher-valid-until">
<p><%= course.attributes.voucher_end_date_text %></p>
</div>
</div>
</div>
</div>
<div class="col-sm-7 col-xs-12">
<p class="course-name"><%= course.attributes.title %></p>
<div class="col-sm-7 col-xs-12">
<p class="course-name"><%= course.attributes.title %></p>
<p class="course-org"><%= course.attributes.organization %></p>
<p class="course-org"><%= course.attributes.organization %></p>
<p class="course-start"><%= course.attributes.course_start_date_text %></p>
<p class="course-start"><%= course.attributes.course_start_date_text %></p>
<div class="discount-mc-price-group clearfix">
<div class="pull-left">
<p class="course-price">
<span>$<%= course.attributes.stockrecords.price_excl_tax %></span>
</p>
</div>
<div class="pull-left">
<p class="course-new-price">
<span><%- gettext('Now') %> $<%= course.attributes.new_price %></span>
</p>
</div>
<div class="pull-right">
<% if (isEnrollmentCode) { %>
<a href="/coupons/redeem/?code=<%= code %>&sku=<%= course.attributes.stockrecords.partner_sku %>"
id="RedeemEnrollment"
class="btn btn-success"><%- gettext('Enroll Now') %></a>
<% } else { %>
<a href="/coupons/redeem/?code=<%= code %>&sku=<%= course.attributes.stockrecords.partner_sku %>"
id="PurchaseCertificate"
class="btn btn-success btn-purchase"
data-track-type="click"
data-track-event="edx.bi.ecommerce.coupons.accept_offer"
data-track-category="coupon-codes"
data-course-id="<%= course.attributes.id %>">
<%- gettext('Checkout') %>
</a>
<% } %>
<div class="discount-mc-price-group clearfix">
<div class="pull-left">
<p class="course-price">
<span>$<%= course.attributes.stockrecords.price_excl_tax %></span>
</p>
</div>
<div class="pull-left">
<p class="course-new-price">
<span><%- gettext('Now') %> $<%= course.attributes.new_price %></span>
</p>
</div>
<div class="pull-right">
<% if (course.isEnrollmentCode) { %>
<a href="/coupons/redeem/?code=<%= code %>&sku=<%= course.attributes.stockrecords.partner_sku %>"
id="RedeemEnrollment"
class="btn btn-success"><%- gettext('Enroll Now') %></a>
<% } else { %>
<a href="/coupons/redeem/?code=<%= code %>&sku=<%= course.attributes.stockrecords.partner_sku %>"
id="PurchaseCertificate"
class="btn btn-success btn-purchase"
data-track-type="click"
data-track-event="edx.bi.ecommerce.coupons.accept_offer"
data-track-category="coupon-codes"
data-course-id="<%= course.attributes.id %>">
<%- gettext('Checkout') %>
</a>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
<% }); %>
<% if(pageInfo.pages > 1) { %>
<% if(courses.numberOfPages > 1) { %>
<div class="pagination-block col-xs-12">
<nav class="pull-right">
<ul class="pagination">
</ul>
</nav>
<h4 class="pagination-range pull-right hidden-xs">Showing <%= pageInfo.range[0] %> - <%= pageInfo.range[1] %> of [<%= pageInfo.total %>]</h4>
<h4 class="pagination-range pull-right hidden-xs">Showing <%= (courses.lowerLimit + 1) %> - <%= courses.upperLimit %> of [<%= courses.length %>]</h4>
</div>
<% } %>
<% } else { %>
......
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