Commit d9c32fcc by Marko Jevtić

Merge pull request #686 from edx/mjevtic/SOL-1589

[SOL-1589] Create Coupon for Multiple Courses UI
parents 8bd2ab94 5e58b69d
...@@ -29,3 +29,16 @@ def get_seats_from_query(site, query, seat_types): ...@@ -29,3 +29,16 @@ def get_seats_from_query(site, query, seat_types):
except Product.DoesNotExist: except Product.DoesNotExist:
pass pass
return query_products return query_products
def prepare_course_seat_types(course_seat_types):
"""
Convert list of course seat types into comma-separated string.
Arguments:
course_seat_types (list): List of course seat types
Returns:
str: Comma-separated list of course seat types if course_seat_types is not empty
"""
return ','.join(seat_type.lower() for seat_type in course_seat_types)
...@@ -6,13 +6,11 @@ class APIDictionaryKeys(object): ...@@ -6,13 +6,11 @@ class APIDictionaryKeys(object):
BASKET_ID = u'id' BASKET_ID = u'id'
BENEFIT_TYPE = u'benefit_type' BENEFIT_TYPE = u'benefit_type'
BENEFIT_VALUE = u'benefit_value' BENEFIT_VALUE = u'benefit_value'
CATALOG_QUERY = u'catalog_query'
CATEGORY_IDS = u'category_ids' CATEGORY_IDS = u'category_ids'
CHECKOUT = u'checkout' CHECKOUT = u'checkout'
CLIENT_USERNAME = u'client_username' CLIENT_USERNAME = u'client_username'
CODE = u'code' CODE = u'code'
COUPON_ID = u'coupon_id' COUPON_ID = u'coupon_id'
COURSE_SEAT_TYPES = u'course_seat_types'
END_DATE = u'end_date' END_DATE = u'end_date'
NOTE = u'note' NOTE = u'note'
ORDER = u'order' ORDER = u'order'
......
...@@ -493,7 +493,8 @@ class CouponSerializer(ProductPaymentInfoMixin, serializers.ModelSerializer): ...@@ -493,7 +493,8 @@ class CouponSerializer(ProductPaymentInfoMixin, serializers.ModelSerializer):
def get_course_seat_types(self, obj): def get_course_seat_types(self, obj):
offer = self.retrieve_offer(obj) offer = self.retrieve_offer(obj)
return offer.condition.range.course_seat_types course_seat_types = offer.condition.range.course_seat_types
return course_seat_types.split(',') if course_seat_types else course_seat_types
class Meta(object): class Meta(object):
model = Product model = Product
......
...@@ -444,7 +444,8 @@ class CouponViewSetFunctionalTest(CouponMixin, CourseCatalogTestMixin, CatalogPr ...@@ -444,7 +444,8 @@ class CouponViewSetFunctionalTest(CouponMixin, CourseCatalogTestMixin, CatalogPr
vouchers = coupon.attr.coupon_vouchers.vouchers.all() vouchers = coupon.attr.coupon_vouchers.vouchers.all()
CouponViewSet().update_coupon_benefit_value( CouponViewSet().update_coupon_benefit_value(
benefit_value=Decimal(54), benefit_value=Decimal(54),
vouchers=vouchers vouchers=vouchers,
coupon=coupon
) )
for voucher in vouchers: for voucher in vouchers:
...@@ -484,15 +485,6 @@ class CouponViewSetFunctionalTest(CouponMixin, CourseCatalogTestMixin, CatalogPr ...@@ -484,15 +485,6 @@ class CouponViewSetFunctionalTest(CouponMixin, CourseCatalogTestMixin, CatalogPr
baskets = Basket.objects.filter(lines__product_id=coupon.id) baskets = Basket.objects.filter(lines__product_id=coupon.id)
self.assertEqual(baskets.first().owner.username, 'Test Client Username') self.assertEqual(baskets.first().owner.username, 'Test Client Username')
def test_exception_for_multi_use_voucher_type(self):
"""Test that an exception is raised for multi-use voucher types."""
self.data.update({
'voucher_type': Voucher.MULTI_USE,
})
with self.assertRaises(NotImplementedError):
self.client.post(COUPONS_LINK, data=self.data, format='json')
@ddt.data('audit', 'honor') @ddt.data('audit', 'honor')
def test_restricted_course_mode(self, mode): def test_restricted_course_mode(self, mode):
"""Test that an exception is raised when a black-listed course mode is used.""" """Test that an exception is raised when a black-listed course mode is used."""
...@@ -525,7 +517,7 @@ class CouponViewSetFunctionalTest(CouponMixin, CourseCatalogTestMixin, CatalogPr ...@@ -525,7 +517,7 @@ class CouponViewSetFunctionalTest(CouponMixin, CourseCatalogTestMixin, CatalogPr
details_response = self.client.get(reverse('api:v2:coupons-detail', args=[coupon_id])) details_response = self.client.get(reverse('api:v2:coupons-detail', args=[coupon_id]))
detail = json.loads(details_response.content) detail = json.loads(details_response.content)
self.assertEqual(detail['catalog_query'], catalog_query) self.assertEqual(detail['catalog_query'], catalog_query)
self.assertEqual(detail['course_seat_types'], course_seat_types[0]) self.assertEqual(detail['course_seat_types'], course_seat_types)
self.assertEqual(detail['seats'][0]['id'], seat.id) self.assertEqual(detail['seats'][0]['id'], seat.id)
......
...@@ -15,6 +15,7 @@ from rest_framework.permissions import IsAdminUser, IsAuthenticated ...@@ -15,6 +15,7 @@ from rest_framework.permissions import IsAdminUser, IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from ecommerce.core.models import BusinessClient, User from ecommerce.core.models import BusinessClient, User
from ecommerce.coupons.utils import prepare_course_seat_types
from ecommerce.extensions.api import data as data_api from ecommerce.extensions.api import data as data_api
from ecommerce.extensions.api.constants import APIConstants as AC from ecommerce.extensions.api.constants import APIConstants as AC
from ecommerce.extensions.api.filters import ProductFilter from ecommerce.extensions.api.filters import ProductFilter
...@@ -35,9 +36,18 @@ Order = get_model('order', 'Order') ...@@ -35,9 +36,18 @@ Order = get_model('order', 'Order')
Product = get_model('catalogue', 'Product') Product = get_model('catalogue', 'Product')
ProductCategory = get_model('catalogue', 'ProductCategory') ProductCategory = get_model('catalogue', 'ProductCategory')
ProductClass = get_model('catalogue', 'ProductClass') ProductClass = get_model('catalogue', 'ProductClass')
Range = Range = get_model('offer', 'Range')
StockRecord = get_model('partner', 'StockRecord') StockRecord = get_model('partner', 'StockRecord')
Voucher = get_model('voucher', 'Voucher') Voucher = get_model('voucher', 'Voucher')
CATALOG_QUERY = 'catalog_query'
COURSE_SEAT_TYPES = 'course_seat_types'
UPDATABLE_RANGE_FIELDS = [
CATALOG_QUERY,
COURSE_SEAT_TYPES,
]
class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet): class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet):
""" Coupon resource. """ """ Coupon resource. """
...@@ -73,7 +83,7 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet): ...@@ -73,7 +83,7 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet):
with transaction.atomic(): with transaction.atomic():
title = request.data[AC.KEYS.TITLE] title = request.data[AC.KEYS.TITLE]
client_username = request.data[AC.KEYS.CLIENT_USERNAME] client_username = request.data[AC.KEYS.CLIENT_USERNAME]
stock_record_ids = request.data.get(AC.KEYS.STOCK_RECORD_IDS, None) stock_record_ids = request.data.get(AC.KEYS.STOCK_RECORD_IDS)
start_date = dateutil.parser.parse(request.data[AC.KEYS.START_DATE]) start_date = dateutil.parser.parse(request.data[AC.KEYS.START_DATE])
end_date = dateutil.parser.parse(request.data[AC.KEYS.END_DATE]) end_date = dateutil.parser.parse(request.data[AC.KEYS.END_DATE])
code = request.data[AC.KEYS.CODE] code = request.data[AC.KEYS.CODE]
...@@ -85,13 +95,13 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet): ...@@ -85,13 +95,13 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet):
partner = request.site.siteconfiguration.partner partner = request.site.siteconfiguration.partner
categories = Category.objects.filter(id__in=request.data[AC.KEYS.CATEGORY_IDS]) categories = Category.objects.filter(id__in=request.data[AC.KEYS.CATEGORY_IDS])
client, __ = BusinessClient.objects.get_or_create(name=client_username) client, __ = BusinessClient.objects.get_or_create(name=client_username)
note = request.data.get('note', None) note = request.data.get('note')
max_uses = request.data.get('max_uses', None) max_uses = request.data.get('max_uses')
catalog_query = request.data.get(AC.KEYS.CATALOG_QUERY, None) catalog_query = request.data.get(CATALOG_QUERY)
course_seat_types = request.data.get(AC.KEYS.COURSE_SEAT_TYPES, None) course_seat_types = request.data.get(COURSE_SEAT_TYPES)
if course_seat_types: if course_seat_types:
course_seat_types = ','.join(seat_type.lower() for seat_type in course_seat_types) course_seat_types = prepare_course_seat_types(course_seat_types)
# Maximum number of uses can be set for each voucher type and disturb # Maximum number of uses can be set for each voucher type and disturb
# the predefined behaviours of the different voucher types. Therefor # the predefined behaviours of the different voucher types. Therefor
...@@ -102,10 +112,6 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet): ...@@ -102,10 +112,6 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet):
else: else:
max_uses = None max_uses = None
# We currently do not support multi-use voucher types.
if voucher_type == Voucher.MULTI_USE:
raise NotImplementedError('Multi-use voucher types are not supported')
# When a black-listed course mode is received raise an exception. # When a black-listed course mode is received raise an exception.
# Audit modes do not have a certificate type and therefore will raise # Audit modes do not have a certificate type and therefore will raise
# an AttributeError exception. # an AttributeError exception.
...@@ -301,9 +307,23 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet): ...@@ -301,9 +307,23 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet):
if data: if data:
vouchers.all().update(**data) vouchers.all().update(**data)
range_data = {}
for field in UPDATABLE_RANGE_FIELDS:
self.create_update_data_dict(
request_data=request.data,
request_data_key=field,
update_dict=range_data,
update_dict_key=field
)
if range_data:
voucher_range = vouchers.first().offers.first().benefit.range
Range.objects.filter(id=voucher_range.id).update(**range_data)
benefit_value = request.data.get(AC.KEYS.BENEFIT_VALUE, '') benefit_value = request.data.get(AC.KEYS.BENEFIT_VALUE, '')
if benefit_value: if benefit_value:
self.update_coupon_benefit_value(benefit_value=benefit_value, vouchers=vouchers) self.update_coupon_benefit_value(benefit_value=benefit_value, vouchers=vouchers, coupon=coupon)
category_ids = request.data.get(AC.KEYS.CATEGORY_IDS, '') category_ids = request.data.get(AC.KEYS.CATEGORY_IDS, '')
if category_ids: if category_ids:
...@@ -336,14 +356,16 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet): ...@@ -336,14 +356,16 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet):
""" """
value = request_data.get(request_data_key, '') value = request_data.get(request_data_key, '')
if value: if value:
update_dict[update_dict_key] = value update_dict[update_dict_key] = prepare_course_seat_types(value) \
if update_dict_key == COURSE_SEAT_TYPES else value
def update_coupon_benefit_value(self, benefit_value, vouchers): def update_coupon_benefit_value(self, benefit_value, vouchers, coupon):
""" """
Remove all offers from the vouchers and add a new offer Remove all offers from the vouchers and add a new offer
Arguments: Arguments:
benefit_value (Decimal): Benefit value associated with a new offer benefit_value (Decimal): Benefit value associated with a new offer
vouchers (ManyRelatedManager): Vouchers associated with the coupon to be updated vouchers (ManyRelatedManager): Vouchers associated with the coupon to be updated
coupon (Product): Coupon product associated with vouchers
""" """
voucher_offers = vouchers.first().offers voucher_offers = vouchers.first().offers
voucher_offer = voucher_offers.first() voucher_offer = voucher_offers.first()
...@@ -351,7 +373,8 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet): ...@@ -351,7 +373,8 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet):
new_offer = update_voucher_offer( new_offer = update_voucher_offer(
offer=voucher_offer, offer=voucher_offer,
benefit_value=benefit_value, benefit_value=benefit_value,
benefit_type=voucher_offer.benefit.type benefit_type=voucher_offer.benefit.type,
coupon=coupon
) )
for voucher in vouchers.all(): for voucher in vouchers.all():
voucher.offers.clear() voucher.offers.clear()
......
...@@ -449,7 +449,7 @@ class UtilTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockMixin, TestCase): ...@@ -449,7 +449,7 @@ class UtilTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockMixin, TestCase):
self.assertEqual(voucher_offer.benefit.value, 100.00) self.assertEqual(voucher_offer.benefit.value, 100.00)
self.assertEqual(voucher_offer.benefit.range.catalog, self.catalog) self.assertEqual(voucher_offer.benefit.range.catalog, self.catalog)
new_offer = update_voucher_offer(voucher_offer, 50.00, Benefit.PERCENTAGE) new_offer = update_voucher_offer(voucher_offer, 50.00, Benefit.PERCENTAGE, self.coupon)
self.assertEqual(new_offer.benefit.type, Benefit.PERCENTAGE) self.assertEqual(new_offer.benefit.type, Benefit.PERCENTAGE)
self.assertEqual(new_offer.benefit.value, 50.00) self.assertEqual(new_offer.benefit.value, 50.00)
self.assertEqual(new_offer.benefit.range.catalog, self.catalog) self.assertEqual(new_offer.benefit.range.catalog, self.catalog)
...@@ -439,7 +439,7 @@ def get_voucher_discount_info(benefit, price): ...@@ -439,7 +439,7 @@ def get_voucher_discount_info(benefit, price):
} }
def update_voucher_offer(offer, benefit_value, benefit_type): def update_voucher_offer(offer, benefit_value, benefit_type, coupon):
""" """
Update voucher offer with new benefit value. Update voucher offer with new benefit value.
...@@ -454,5 +454,6 @@ def update_voucher_offer(offer, benefit_value, benefit_type): ...@@ -454,5 +454,6 @@ def update_voucher_offer(offer, benefit_value, benefit_type):
return _get_or_create_offer( return _get_or_create_offer(
product_range=offer.benefit.range, product_range=offer.benefit.range,
benefit_value=benefit_value, benefit_value=benefit_value,
benefit_type=benefit_type benefit_type=benefit_type,
coupon_id=coupon.id
) )
...@@ -21,9 +21,10 @@ define([ ...@@ -21,9 +21,10 @@ define([
'use strict'; 'use strict';
_.extend(Backbone.Validation.messages, { _.extend(Backbone.Validation.messages, {
required: gettext('This field is required'), required: gettext('This field is required.'),
number: gettext('This value must be a number'), number: gettext('This value must be a number.'),
date: gettext('This value must be a date') date: gettext('This value must be a date.'),
seat_types: gettext('At least one seat type must be selected.'),
}); });
_.extend(Backbone.Model.prototype, Backbone.Validation.mixin); _.extend(Backbone.Model.prototype, Backbone.Validation.mixin);
...@@ -37,19 +38,29 @@ define([ ...@@ -37,19 +38,29 @@ define([
code: '', code: '',
price: 0, price: 0,
total_value: 0, total_value: 0,
max_uses: 1 max_uses: 1,
seats: [],
course_seats: [],
course_seat_types: []
}, },
validation: { validation: {
category: {required: true}, category: {required: true},
course_id: { course_id: {
pattern: 'courseId', pattern: 'courseId',
msg: gettext('A valid course ID is required') msg: gettext('A valid course ID is required'),
required: function () {
return this.get('catalog_type') === 'Single course';
}
}, },
title: {required: true}, title: {required: true},
client: {required: true}, client: {required: true},
// seat_type is for validation only, stock_record_ids holds the values // seat_type is for validation only, stock_record_ids holds the values
seat_type: {required: true}, seat_type: {
required: function () {
return this.get('catalog_type') === 'Single course';
}
},
quantity: {pattern: 'number'}, quantity: {pattern: 'number'},
price: {pattern: 'number'}, price: {pattern: 'number'},
benefit_value: { benefit_value: {
...@@ -63,6 +74,16 @@ define([ ...@@ -63,6 +74,16 @@ define([
rangeLength: [8, 16], rangeLength: [8, 16],
msg: gettext('Code field must be empty or between 8 and 16 characters') msg: gettext('Code field must be empty or between 8 and 16 characters')
}, },
catalog_query: {
required: function () {
return this.get('catalog_type') === 'Multiple courses';
}
},
course_seat_types: function (val) {
if (this.get('catalog_type') === 'Multiple courses' && val.length === 0) {
return Backbone.Validation.messages.seat_types;
}
},
start_date: function (val) { start_date: function (val) {
var startDate, var startDate,
endDate; endDate;
...@@ -112,20 +133,39 @@ define([ ...@@ -112,20 +133,39 @@ define([
}, },
getSeatPrice: function () { getSeatPrice: function () {
return this.get('seats')[0].price; var seats = this.get('seats');
return seats[0] ? seats[0].price : '';
}, },
updateTotalValue: function (seat_price) { updateTotalValue: function (seat_price) {
this.set('total_value', this.get('quantity') * seat_price); this.set('total_value', this.get('quantity') * seat_price);
}, },
getCertificateType: function(seat_data) {
var seat_type = _.findWhere(seat_data, {'name': 'certificate_type'});
return seat_type ? seat_type.value : '';
},
getCourseID: function(seat_data) {
var course_id = _.findWhere(seat_data, {'name': 'course_key'});
return course_id ? course_id.value : '';
},
updateSeatData: function () { updateSeatData: function () {
var seat_data = this.get('seats')[0].attribute_values, var seat_data,
seat_type = _.findWhere(seat_data, {'name': 'certificate_type'}), seats = this.get('seats');
course_id = _.findWhere(seat_data, {'name': 'course_key'});
this.set('seat_type', seat_type ? seat_type.value : ''); this.set('catalog_type', this.has('catalog_query') ? 'Multiple courses': 'Single course');
this.set('course_id', course_id ? course_id.value : '');
this.updateTotalValue(this.getSeatPrice()); if (this.get('catalog_type') === 'Single course') {
if (seats[0]) {
seat_data = seats[0].attribute_values;
this.set('seat_type', this.getCertificateType(seat_data));
this.set('course_id', this.getCourseID(seat_data));
this.updateTotalValue(this.getSeatPrice());
}
}
}, },
updateVoucherData: function () { updateVoucherData: function () {
......
...@@ -16,7 +16,7 @@ define([ ...@@ -16,7 +16,7 @@ define([
initialize: function (options) { initialize: function (options) {
this.model = Coupon.findOrCreate({id: options.id}); this.model = Coupon.findOrCreate({id: options.id});
this.view = new CouponDetailView({model: this.model}); this.view = new CouponDetailView({model: this.model});
this.listenTo(this.model, 'sync', this.render); this.listenTo(this.model, 'sync', this.refresh);
this.model.fetch(); this.model.fetch();
} }
}); });
......
...@@ -63,6 +63,11 @@ define(['backbone', ...@@ -63,6 +63,11 @@ define(['backbone',
this.renderTitle(); this.renderTitle();
this.renderNestedView(); this.renderNestedView();
return this; return this;
},
refresh: function () {
this.view.remove();
this.render();
} }
}); });
......
define([ define([
'backbone' 'backbone',
'backbone.route-filter',
'backbone.super'
], ],
function (Backbone) { function (Backbone) {
'use strict'; 'use strict';
...@@ -8,7 +10,6 @@ define([ ...@@ -8,7 +10,6 @@ define([
* Base Router class. * Base Router class.
*/ */
return Backbone.Router.extend({ return Backbone.Router.extend({
// Keeps track of the page/view currently on display // Keeps track of the page/view currently on display
currentView: null, currentView: null,
......
...@@ -17,9 +17,14 @@ define([ ...@@ -17,9 +17,14 @@ define([
describe('Coupon model', function () { describe('Coupon model', function () {
describe('validation', function () { describe('validation', function () {
it('should validate dates', function () { var model;
beforeEach(function () {
spyOn($, 'ajax'); spyOn($, 'ajax');
var model = Coupon.findOrCreate(discountCodeData, {parse: true}); model = Coupon.findOrCreate(discountCodeData, {parse: true});
});
it('should validate dates', function () {
model.validate(); model.validate();
expect(model.isValid()).toBeTruthy(); expect(model.isValid()).toBeTruthy();
...@@ -35,13 +40,61 @@ define([ ...@@ -35,13 +40,61 @@ define([
}); });
it('should validate discount code has discount type and value', function () { it('should validate discount code has discount type and value', function () {
spyOn($, 'ajax');
var model = Coupon.findOrCreate(discountCodeData, {parse: true});
model.set('benefit_value', ''); model.set('benefit_value', '');
model.set('benefit_type', ''); model.set('benefit_type', '');
model.validate(); model.validate();
expect(model.isValid()).toBeFalsy(); expect(model.isValid()).toBeFalsy();
}); });
it('should validate course ID if the catalog is a Single Course Catalog', function () {
model.set('catalog_type', 'Single course');
model.set('course_id', '');
model.validate();
expect(model.isValid()).toBeFalsy();
model.set('course_id', 'a/b/c');
model.validate();
expect(model.isValid()).toBeTruthy();
});
it('should validate seat type if the catalog is a Single Course Catalog', function () {
model.set('catalog_type', 'Single course');
model.set('seat_type', '');
model.validate();
expect(model.isValid()).toBeFalsy();
model.set('seat_type', 'Verified');
model.validate();
expect(model.isValid()).toBeTruthy();
});
it('should validate catalog query and course seat types for Multiple Courses Catalog', function () {
model.set('catalog_type', 'Multiple courses');
model.set('catalog_query', '');
model.validate();
expect(model.isValid()).toBeFalsy();
model.set('catalog_query', '*:*');
model.set('course_seat_types', []);
model.validate();
expect(model.isValid()).toBeFalsy();
model.set('catalog_query', '*:*');
model.set('course_seat_types', ['verified']);
model.validate();
expect(model.isValid()).toBeTruthy();
});
});
describe('test model methods', function () {
it('should return seat price if a coupon has a seat', function () {
var model = new Coupon();
expect(model.getSeatPrice()).toEqual('');
model.set('seats', [{'price': 100}]);
expect(model.getSeatPrice()).toEqual(100);
});
}); });
describe('save', function () { describe('save', function () {
......
define([
'backbone',
'jquery',
'pages/page'
],
function(Backbone,
$,
Page
) {
'use strict';
describe('Base page', function() {
var page;
it('should remove and render the view when refresh is called', function() {
page = new Page();
page.view = new Backbone.View();
spyOn(page.view, 'remove');
spyOn(page, 'render');
page.refresh();
expect(page.view.remove).toHaveBeenCalled();
expect(page.render).toHaveBeenCalled();
});
});
});
...@@ -35,11 +35,6 @@ define([ ...@@ -35,11 +35,6 @@ define([
verifiedSeat = Mock_Coupons.verifiedSeat; verifiedSeat = Mock_Coupons.verifiedSeat;
}); });
it('should capitalize string value', function () {
expect(view.capitalize('abc')).toBe('Abc');
expect(view.capitalize('aBC')).toBe('Abc');
});
it('should get code status from voucher data', function () { it('should get code status from voucher data', function () {
expect(view.codeStatus(enrollmentCodeVoucher)).toBe('ACTIVE'); expect(view.codeStatus(enrollmentCodeVoucher)).toBe('ACTIVE');
...@@ -56,38 +51,6 @@ define([ ...@@ -56,38 +51,6 @@ define([
expect(view.couponType(enrollmentCodeVoucher)).toBe('Enrollment Code'); expect(view.couponType(enrollmentCodeVoucher)).toBe('Enrollment Code');
}); });
it('should get course ID from seat data', function () {
expect(view.courseID(verifiedSeat.attribute_values)).toBe('course-v1:edX+DemoX+Demo_Course');
verifiedSeat.attribute_values = [
{
name: 'certificate_type',
value: 'verified'
},
{
name: 'id_verification_required',
value: true
}
];
expect(view.courseID(verifiedSeat.attribute_values)).toBe('');
});
it('should get certificate type from seat data', function () {
expect(view.certificateType(verifiedSeat.attribute_values)).toBe('Verified');
verifiedSeat.attribute_values = [
{
name: 'course_key',
value: 'course-v1:edX+DemoX+Demo_Course'
},
{
name: 'id_verification_required',
value: true
}
];
expect(view.certificateType(verifiedSeat.attribute_values)).toBe('');
});
it('should get discount value from voucher data', function () { it('should get discount value from voucher data', function () {
expect(view.discountValue(percentageDiscountCodeVoucher)).toBe('50%'); expect(view.discountValue(percentageDiscountCodeVoucher)).toBe('50%');
expect(view.discountValue(valueDiscountCodeVoucher)).toBe('$12'); expect(view.discountValue(valueDiscountCodeVoucher)).toBe('$12');
...@@ -106,13 +69,17 @@ define([ ...@@ -106,13 +69,17 @@ define([
expect(view.usageLimitation(enrollmentCodeVoucher)).toBe('Can be used once by one customer'); expect(view.usageLimitation(enrollmentCodeVoucher)).toBe('Can be used once by one customer');
expect(view.usageLimitation(valueDiscountCodeVoucher)).toBe('Can be used once by multiple customers'); expect(view.usageLimitation(valueDiscountCodeVoucher)).toBe('Can be used once by multiple customers');
valueDiscountCodeVoucher.usage = 'Multi-use';
expect(view.usageLimitation(valueDiscountCodeVoucher)).toBe(
'Can be used multiple times by multiple customers'
);
valueDiscountCodeVoucher.usage = ''; valueDiscountCodeVoucher.usage = '';
expect(view.usageLimitation(valueDiscountCodeVoucher)).toBe(''); expect(view.usageLimitation(valueDiscountCodeVoucher)).toBe('');
}); });
it('should display correct data upon rendering', function () { it('should display correct data upon rendering', function () {
var course_data = model.get('seats')[0].attribute_values, var voucher = model.get('vouchers')[0],
voucher = model.get('vouchers')[0],
category = model.get('categories')[0].name; category = model.get('categories')[0].name;
spyOn(view, 'renderVoucherTable'); spyOn(view, 'renderVoucherTable');
...@@ -125,10 +92,10 @@ define([ ...@@ -125,10 +92,10 @@ define([
); );
expect(view.$el.find('.category > .value').text()).toEqual(category); expect(view.$el.find('.category > .value').text()).toEqual(category);
expect(view.$el.find('.discount-value > .value').text()).toEqual(view.discountValue(voucher)); expect(view.$el.find('.discount-value > .value').text()).toEqual(view.discountValue(voucher));
expect($.trim(view.$el.find('.course-info > .value').text())).toEqual(view.courseID(course_data)); expect(view.$el.find('.course-info > .value').contents().get(0).nodeValue).toEqual(
expect(view.$el.find('.course-info > .value > .pull-right').text()).toEqual( 'course-v1:edX+DemoX+Demo_Course'
view.certificateType(course_data)
); );
expect(view.$el.find('.course-info > .value > .pull-right').text()).toEqual('verified');
expect(view.$el.find('.start-date-info > .value').text()).toEqual( expect(view.$el.find('.start-date-info > .value').text()).toEqual(
view.formatDateTime(voucher.start_datetime) view.formatDateTime(voucher.start_datetime)
); );
...@@ -143,6 +110,31 @@ define([ ...@@ -143,6 +110,31 @@ define([
expect(view.renderVoucherTable).toHaveBeenCalled(); expect(view.renderVoucherTable).toHaveBeenCalled();
}); });
it('should render course data', function () {
view.model.set({
'catalog_type': 'Single course',
'course_id': 'a/b/c',
'seat_type': 'Verified'
});
view.render();
var course_info = view.$el.find('.course-info .value');
expect(course_info.length).toEqual(1);
expect(course_info.text()).toEqual('a/b/cVerified');
view.model.set({
'catalog_type': 'Multiple courses',
'catalog_query': 'id:*',
'course_seat_types': ['verified', 'professional']
});
view.render();
expect(view.$el.find('.catalog-query .value').text()).toEqual('id:*');
expect(view.$el.find('.seat-types .value').text()).toEqual('verified,professional');
});
it('should display data table', function () { it('should display data table', function () {
view.renderVoucherTable(); view.renderVoucherTable();
expect(view.$el.find('#vouchersTable').DataTable().autowidth).toBeFalsy(); expect(view.$el.find('#vouchersTable').DataTable().autowidth).toBeFalsy();
......
...@@ -36,7 +36,7 @@ define([ ...@@ -36,7 +36,7 @@ define([
expect(view.$el.find('[name=code_type]').val()).toEqual('Enrollment code'); expect(view.$el.find('[name=code_type]').val()).toEqual('Enrollment code');
expect(view.$el.find('[name=start_date]').val()).toEqual(startDate); expect(view.$el.find('[name=start_date]').val()).toEqual(startDate);
expect(view.$el.find('[name=end_date]').val()).toEqual(endDate); expect(view.$el.find('[name=end_date]').val()).toEqual(endDate);
expect(voucherType.children().length).toBe(2); expect(voucherType.children().length).toBe(3);
expect(voucherType.val()).toEqual(model.get('voucher_type')); expect(voucherType.val()).toEqual(model.get('voucher_type'));
expect(view.$el.find('[name=quantity]').val()).toEqual(model.get('quantity').toString()); expect(view.$el.find('[name=quantity]').val()).toEqual(model.get('quantity').toString());
expect(view.$el.find('[name=client]').val()).toEqual(model.get('client')); expect(view.$el.find('[name=client]').val()).toEqual(model.get('client'));
...@@ -61,7 +61,7 @@ define([ ...@@ -61,7 +61,7 @@ define([
expect(view.$el.find('[name=code_type]').val()).toEqual('Discount code'); expect(view.$el.find('[name=code_type]').val()).toEqual('Discount code');
expect(view.$el.find('[name=start_date]').val()).toEqual(startDate); expect(view.$el.find('[name=start_date]').val()).toEqual(startDate);
expect(view.$el.find('[name=end_date]').val()).toEqual(endDate); expect(view.$el.find('[name=end_date]').val()).toEqual(endDate);
expect(voucherType.children().length).toBe(2); expect(voucherType.children().length).toBe(3);
expect(voucherType.val()).toEqual(model.get('voucher_type')); expect(voucherType.val()).toEqual(model.get('voucher_type'));
expect(view.$el.find('[name=quantity]').val()).toEqual(model.get('quantity').toString()); expect(view.$el.find('[name=quantity]').val()).toEqual(model.get('quantity').toString());
expect(view.$el.find('[name=client]').val()).toEqual(model.get('client')); expect(view.$el.find('[name=client]').val()).toEqual(model.get('client'));
......
...@@ -119,7 +119,7 @@ define([ ...@@ -119,7 +119,7 @@ define([
}); });
}); });
describe('discount', function () { describe('discount code', function () {
beforeEach(function () { beforeEach(function () {
view.$el.find('[name=code_type]').val('Discount code').trigger('change'); view.$el.find('[name=code_type]').val('Discount code').trigger('change');
}); });
...@@ -179,6 +179,20 @@ define([ ...@@ -179,6 +179,20 @@ define([
expect(visible('[name=code]')).toBe(true); expect(visible('[name=code]')).toBe(true);
}); });
}); });
describe('dynamic catalog coupon', function () {
it('should update dynamic catalog view query with coupon catalog query', function() {
model.set('catalog_query', '*:*');
view.updateCatalogQuery();
expect(view.dynamic_catalog_view.query).toEqual(model.get('catalog_query'));
});
it('should update dynamic catalog view course seat types with coupon seat types', function() {
model.set('course_seat_types', ['verified']);
view.updateCourseSeatTypes();
expect(view.dynamic_catalog_view.seat_types).toEqual(model.get('course_seat_types'));
});
});
}); });
} }
); );
define([
'jquery',
'underscore',
'collections/course_collection',
'models/course_model',
'views/dynamic_catalog_view'
],
function ($,
_,
Courses,
Course,
DynamicCatalogView) {
'use strict';
describe('dynamic catalog view', function () {
var view;
beforeEach(function () {
view = new DynamicCatalogView({
creating_editing: true,
query: '*:*',
seat_types: 'verified,professional'
});
view.render();
});
it('should call preview catalog if preview button was clicked', function () {
spyOn(view, 'previewCatalog');
view.delegateEvents();
view.$el.find('[name=preview_catalog]').trigger('click');
expect(view.previewCatalog).toHaveBeenCalled();
});
it('should format row data for dynamic catalog preview', function () {
var course = Course.findOrCreate({
'id': 'a/b/c',
'name': 'ABC Course',
'type': 'verified'
}, {parse: true}),
row_data = view.getRowData(course);
expect(row_data).toEqual({
'id': course.get('id'),
'name': course.get('name'),
'type': 'Verified'
});
});
it('should call Course Catalog API if previewCatalog was called', function () {
var args,
calls,
e = $.Event('click');
spyOn(e, 'preventDefault');
spyOn(Backbone, 'ajax');
view.previewCatalog(e);
expect(e.preventDefault).toHaveBeenCalled();
expect(Backbone.ajax).toHaveBeenCalled();
calls = Backbone.ajax.calls;
args = calls.argsFor(calls.count() - 1)[0];
expect(args.type).toEqual('GET');
expect(args.url).toEqual(window.location.origin + '/api/v2/catalogs/preview/');
expect(args.data).toEqual({query: view.query});
expect(args.success).toEqual(view.onSuccess);
});
it('should fill datatable on successful AJAX call to Course Catalog API', function () {
var API_data = {
results: [{
key: 'a/b/c'
}, {
key: 'd/e/f'
}]
},
args;
spyOn(_, 'pluck');
spyOn($.prototype, 'DataTable');
view.onSuccess(API_data);
expect(_.pluck).toHaveBeenCalledWith(API_data.results, 'key');
expect($.prototype.DataTable).toHaveBeenCalled();
args = $.prototype.DataTable.calls.argsFor(0)[0];
expect(args.autoWidth).toBeFalsy();
expect(args.destroy).toBeTruthy();
expect(args.info).toBeTruthy();
expect(args.paging).toBeTruthy();
expect(args.ordering).toBeFalsy();
expect(args.searching).toBeFalsy();
expect(args.columns).toEqual([
{title: 'Course ID', data: 'id'},
{title: 'Course name', data: 'name'},
{title: 'Seat type', data: 'type'}
]);
});
it('should filter courses by calling filterCourses function', function() {
var course_keys = ['test1/test1/test1', 'test3/test3/test3'],
filtered_courses,
seat_types = ['verified'];
view.courses = new Courses([
Course.findOrCreate({id: 'test1/test1/test1', type: 'verified'}),
Course.findOrCreate({id: 'test2/test2/test2', type: 'verified'}),
Course.findOrCreate({id: 'test3/test3/test3', type: 'professional'})
]);
filtered_courses = view.filterCourses(course_keys, seat_types);
expect(filtered_courses.length).toEqual(1);
expect(filtered_courses[0].get('id')).toEqual('test1/test1/test1');
expect(filtered_courses[0].get('type')).toEqual('verified');
});
});
}
);
...@@ -4,14 +4,16 @@ define([ ...@@ -4,14 +4,16 @@ define([
'underscore', 'underscore',
'underscore.string', 'underscore.string',
'moment', 'moment',
'text!templates/coupon_detail.html' 'text!templates/coupon_detail.html',
'views/dynamic_catalog_view',
], ],
function ($, function ($,
Backbone, Backbone,
_, _,
_s, _s,
moment, moment,
CouponDetailTemplate) { CouponDetailTemplate,
DynamicCatalogView) {
'use strict'; 'use strict';
return Backbone.View.extend({ return Backbone.View.extend({
...@@ -23,10 +25,6 @@ define([ ...@@ -23,10 +25,6 @@ define([
template: _.template(CouponDetailTemplate), template: _.template(CouponDetailTemplate),
capitalize: function (string) {
return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase();
},
codeStatus: function (voucher) { codeStatus: function (voucher) {
var startDate = moment(new Date(voucher.start_datetime)), var startDate = moment(new Date(voucher.start_datetime)),
endDate = moment(new Date(voucher.end_datetime)), endDate = moment(new Date(voucher.end_datetime)),
...@@ -42,16 +40,6 @@ define([ ...@@ -42,16 +40,6 @@ define([
); );
}, },
courseID: function(course_data) {
var course_id = _.findWhere(course_data, {'name': 'course_key'});
return course_id ? course_id.value : '';
},
certificateType: function(course_data) {
var certificate_type = _.findWhere(course_data, {'name': 'certificate_type'});
return certificate_type ? gettext(this.capitalize(certificate_type.value)) : '';
},
discountValue: function(voucher) { discountValue: function(voucher) {
var benefitType = voucher.benefit[0], var benefitType = voucher.benefit[0],
benefitValue = voucher.benefit[1], benefitValue = voucher.benefit[1],
...@@ -70,6 +58,8 @@ define([ ...@@ -70,6 +58,8 @@ define([
usageLimitation: function(voucher) { usageLimitation: function(voucher) {
if (voucher.usage === 'Single use') { if (voucher.usage === 'Single use') {
return gettext('Can be used once by one customer'); return gettext('Can be used once by one customer');
} else if (voucher.usage === 'Multi-use') {
return gettext('Can be used multiple times by multiple customers');
} else if (voucher.usage === 'Once per customer') { } else if (voucher.usage === 'Once per customer') {
return gettext('Can be used once by multiple customers'); return gettext('Can be used once by multiple customers');
} }
...@@ -77,16 +67,13 @@ define([ ...@@ -77,16 +67,13 @@ define([
}, },
render: function () { render: function () {
var course_data = this.model.get('seats')[0].attribute_values, var html,
html,
voucher = this.model.get('vouchers')[0], voucher = this.model.get('vouchers')[0],
category = this.model.get('categories')[0].name, category = this.model.get('categories')[0].name,
note = this.model.get('note'); note = this.model.get('note');
html = this.template({ html = this.template({
course_id: this.courseID(course_data), coupon: this.model.toJSON(),
certificate_type: this.certificateType(course_data),
coupon: this.model.attributes,
couponType: this.couponType(voucher), couponType: this.couponType(voucher),
codeStatus: this.codeStatus(voucher), codeStatus: this.codeStatus(voucher),
discountValue: this.discountValue(voucher), discountValue: this.discountValue(voucher),
...@@ -101,7 +88,17 @@ define([ ...@@ -101,7 +88,17 @@ define([
this.$el.html(html); this.$el.html(html);
this.renderVoucherTable(); this.renderVoucherTable();
this.renderCourseData();
this.delegateEvents(); this.delegateEvents();
this.dynamic_catalog_view = new DynamicCatalogView({
'query': this.model.get('catalog_query'),
'seat_types': this.model.get('course_seat_types')
});
this.dynamic_catalog_view.$el = this.$('.catalog_buttons');
this.dynamic_catalog_view.render();
this.dynamic_catalog_view.delegateEvents();
return this; return this;
}, },
...@@ -127,6 +124,26 @@ define([ ...@@ -127,6 +124,26 @@ define([
return this; return this;
}, },
renderCourseData: function () {
if (this.model.get('catalog_type') === 'Single course') {
this.$el.find('.course-info').append(
_s.sprintf(
'<div class="value">%s<span class="pull-right">%s</span></div>',
this.model.get('course_id'),
this.model.get('seat_type'))
);
this.$el.find('.catalog-query').addClass('hidden');
this.$el.find('.seat-types').addClass('hidden');
this.$el.find('.course-info').removeClass('hidden');
} else if (this.model.get('catalog_type') === 'Multiple courses') {
this.$el.find('.course-info').addClass('hidden');
this.$el.find('.catalog-query').removeClass('hidden');
this.$el.find('.seat-types').removeClass('hidden');
}
return this;
},
downloadCouponReport: function (event) { downloadCouponReport: function (event) {
var url = _s.sprintf('/api/v2/coupons/coupon_reports/%d', this.model.id); var url = _s.sprintf('/api/v2/coupons/coupon_reports/%d', this.model.id);
......
...@@ -12,7 +12,9 @@ define([ ...@@ -12,7 +12,9 @@ define([
'utils/utils', 'utils/utils',
'text!templates/coupon_form.html', 'text!templates/coupon_form.html',
'models/course_model', 'models/course_model',
'views/form_view' 'collections/course_collection',
'views/form_view',
'views/dynamic_catalog_view',
], ],
function ($, function ($,
Backbone, Backbone,
...@@ -25,7 +27,9 @@ define([ ...@@ -25,7 +27,9 @@ define([
Utils, Utils,
CouponFormTemplate, CouponFormTemplate,
Course, Course,
FormView) { Courses,
FormView,
DynamicCatalogView) {
'use strict'; 'use strict';
return FormView.extend({ return FormView.extend({
...@@ -56,6 +60,10 @@ define([ ...@@ -56,6 +60,10 @@ define([
{ {
value: 'Once per customer', value: 'Once per customer',
label: gettext('Can be used once by multiple customers') label: gettext('Can be used once by multiple customers')
},
{
value: 'Multi-use',
label: gettext('Can be used multiple times by multiple customers'),
} }
], ],
...@@ -152,7 +160,16 @@ define([ ...@@ -152,7 +160,16 @@ define([
}, },
'input[name=max_uses]': { 'input[name=max_uses]': {
observe: 'max_uses' observe: 'max_uses'
} },
'input[name=catalog_type]': {
observe: 'catalog_type'
},
'textarea[name=catalog_query]': {
observe: 'catalog_query'
},
'input[name=course_seat_types]': {
observe: 'course_seat_types'
},
}, },
events: { events: {
...@@ -170,10 +187,18 @@ define([ ...@@ -170,10 +187,18 @@ define([
this.editing = options.editing || false; this.editing = options.editing || false;
this.hiddenClass = 'hidden'; this.hiddenClass = 'hidden';
this.dynamic_catalog_view = new DynamicCatalogView({
'query': this.model.get('catalog_query'),
'seat_types': this.model.get('course_seat_types')
});
this.listenTo(this.model, 'change:coupon_type', this.toggleCouponTypeField); this.listenTo(this.model, 'change:coupon_type', this.toggleCouponTypeField);
this.listenTo(this.model, 'change:voucher_type', this.toggleVoucherTypeField); this.listenTo(this.model, 'change:voucher_type', this.toggleVoucherTypeField);
this.listenTo(this.model, 'change:code', this.toggleCodeField); this.listenTo(this.model, 'change:code', this.toggleCodeField);
this.listenTo(this.model, 'change:quantity', this.toggleQuantityField); this.listenTo(this.model, 'change:quantity', this.toggleQuantityField);
this.listenTo(this.model, 'change:catalog_type', this.toggleCatalogTypeField);
this.listenTo(this.model, 'change:catalog_query', this.updateCatalogQuery);
this.listenTo(this.model, 'change:course_seat_types', this.updateCourseSeatTypes);
this._super(); this._super();
}, },
...@@ -216,6 +241,20 @@ define([ ...@@ -216,6 +241,20 @@ define([
} }
}, },
toggleCatalogTypeField: function () {
if (this.model.get('catalog_type') === 'Single course') {
this.formGroup('[name=catalog_query]').addClass(this.hiddenClass);
this.formGroup('[name=course_seat_types]').addClass(this.hiddenClass);
this.formGroup('[name=course_id]').removeClass(this.hiddenClass);
this.formGroup('[name=seat_type]').removeClass(this.hiddenClass);
} else {
this.formGroup('[name=catalog_query]').removeClass(this.hiddenClass);
this.formGroup('[name=course_seat_types]').removeClass(this.hiddenClass);
this.formGroup('[name=course_id]').addClass(this.hiddenClass);
this.formGroup('[name=seat_type]').addClass(this.hiddenClass);
}
},
toggleVoucherTypeField: function () { toggleVoucherTypeField: function () {
var voucherType = this.model.get('voucher_type'); var voucherType = this.model.get('voucher_type');
if (!this.editing) { if (!this.editing) {
...@@ -343,6 +382,7 @@ define([ ...@@ -343,6 +382,7 @@ define([
this.$el.find('input[name=benefit_type]').attr('disabled', true); this.$el.find('input[name=benefit_type]').attr('disabled', true);
this.$el.find('select[name=seat_type]').attr('disabled', true); this.$el.find('select[name=seat_type]').attr('disabled', true);
this.$el.find('input[name=max_uses]').attr('disabled', true); this.$el.find('input[name=max_uses]').attr('disabled', true);
this.$el.find('input[name=catalog_type]').attr('disabled', true);
}, },
getSeatData: function () { getSeatData: function () {
...@@ -353,11 +393,22 @@ define([ ...@@ -353,11 +393,22 @@ define([
return this.$el.find('[name=seat_type]').val(); return this.$el.find('[name=seat_type]').val();
}, },
updateCatalogQuery: function() {
this.dynamic_catalog_view.query = this.model.get('catalog_query');
},
updateCourseSeatTypes: function() {
this.dynamic_catalog_view.seat_types = this.model.get('course_seat_types');
},
render: function () { render: function () {
// Render the parent form/template // Render the parent form/template
this.$el.html(this.template(this.model.attributes)); this.$el.html(this.template(this.model.attributes));
this.stickit(); this.stickit();
this.toggleCatalogTypeField();
this.dynamic_catalog_view.setElement(this.$el.find('.catalog_buttons')).render();
// Avoid the need to create this jQuery object every time an alert has to be rendered. // Avoid the need to create this jQuery object every time an alert has to be rendered.
this.$alerts = this.$el.find('.alerts'); this.$alerts = this.$el.find('.alerts');
...@@ -368,6 +419,7 @@ define([ ...@@ -368,6 +419,7 @@ define([
this.toggleVoucherTypeField(); this.toggleVoucherTypeField();
this.toggleCodeField(); this.toggleCodeField();
this.toggleQuantityField(); this.toggleQuantityField();
this.$el.find('.catalog-query').addClass('editing');
this.$el.find('button[type=submit]').html(gettext('Save Changes')); this.$el.find('button[type=submit]').html(gettext('Save Changes'));
this.fillFromCourse(); this.fillFromCourse();
} else { } else {
...@@ -377,8 +429,10 @@ define([ ...@@ -377,8 +429,10 @@ define([
this.model.set('voucher_type', this.voucherTypes[0].value); this.model.set('voucher_type', this.voucherTypes[0].value);
this.model.set('category', defaultCategory[0].id); this.model.set('category', defaultCategory[0].id);
this.model.set('benefit_type', 'Percentage'); this.model.set('benefit_type', 'Percentage');
this.model.set('catalog_type', 'Single course');
this.$el.find('[name=benefit_value]').attr('max', 100); this.$el.find('[name=benefit_value]').attr('max', 100);
this.$el.find('button[type=submit]').html(gettext('Create Coupon')); this.$el.find('button[type=submit]').html(gettext('Create Coupon'));
this.$el.find('.catalog-query').removeClass('editing');
} }
// Add date picker // Add date picker
......
define(['jquery',
'backbone',
'underscore.string',
'collections/course_collection',
'text!templates/dynamic_catalog_buttons.html'
],
function ($,
Backbone,
_s,
Courses,
DynamicCatalogButtons) {
'use strict';
return Backbone.View.extend({
template: _.template(DynamicCatalogButtons),
events: {
'click [name=preview_catalog]': 'previewCatalog'
},
initialize: function (options) {
this.query = options.query;
this.seat_types = options.seat_types;
this.courses = new Courses();
this._super();
},
getRowData: function (course) {
return {
id: course.get('id'),
name: course.get('name'),
type: _s(course.get('type')).capitalize().value()
};
},
previewCatalog: function (event) {
event.preventDefault();
this.courses.fetch();
Backbone.ajax({
context: this,
type: 'GET',
url: window.location.origin + '/api/v2/catalogs/preview/',
data: {
query : this.query
},
success: this.onSuccess
});
},
filterCourses: function (course_keys, seat_types) {
return _.filter(this.courses.models, function(course) {
return (_.contains(course_keys, course.get('id')) && _.contains(seat_types, course.get('type')));
});
},
onSuccess: function(data) {
var course_keys = _.pluck(data.results, 'key'),
course_data = this.filterCourses(course_keys, this.seat_types);
this.$el.find('#coursesTable').DataTable({
autoWidth: false,
destroy: true,
info: true,
paging: true,
ordering: false,
searching: false,
columns: [
{
title: gettext('Course ID'),
data: 'id'
},
{
title: gettext('Course name'),
data: 'name'
},
{
title: gettext('Seat type'),
data: 'type'
}
],
data: course_data.map(this.getRowData, this)
}, this);
},
render: function () {
this.$el.html(this.template({}));
return this;
}
});
});
...@@ -210,9 +210,9 @@ define([ ...@@ -210,9 +210,9 @@ define([
* Override Backbone.View.extend so that the child view inherits events. * Override Backbone.View.extend so that the child view inherits events.
*/ */
FormView.extend = function (child) { FormView.extend = function (child) {
var view = Backbone.View.extend.apply(this, arguments); var view = Backbone.View.extend.apply(this, arguments);
view.prototype.events = _.extend({}, this.prototype.events, child.events); view.prototype.events = _.extend({}, this.prototype.events, child.events);
return view; return view;
}; };
return FormView; return FormView;
......
...@@ -12,6 +12,54 @@ ...@@ -12,6 +12,54 @@
.help-block { .help-block {
margin: 0; margin: 0;
} }
.catalog-type {
padding-top: 30px;
height: 61px;
label {
padding: 0 10px;
}
}
.catalog-query {
margin-bottom: 10px;
&.editing {
margin-bottom: 30px;
}
p {
margin-bottom: 20px;
}
button {
margin-top: 10px;
margin-right: 10px;
}
textarea {
height: 216px;
}
}
.course-seat-types {
margin-bottom: 27px;
height: 154px;
.form-inline label {
margin-bottom: 10px;
padding-right: 10px;
}
.checkboxes {
margin-bottom: 25px;
}
.catalog-buttons {
margin-bottom: 34px;
}
}
} }
.coupon-detail-view { .coupon-detail-view {
...@@ -59,7 +107,9 @@ ...@@ -59,7 +107,9 @@
.date-info, .date-info,
.usage-limitations, .usage-limitations,
.client-info, .client-info,
.total-paid { .total-paid,
.catalog-query,
.seat-types {
@include float(left); @include float(left);
@include margin-right(2%); @include margin-right(2%);
...@@ -68,12 +118,15 @@ ...@@ -68,12 +118,15 @@
height: 75px; height: 75px;
} }
.date-info,
.usage-limitations, .usage-limitations,
.total-paid { .total-paid {
@include margin-right(50%); @include margin-right(50%);
} }
.date-info.single-course {
margin-right: 50%;
}
.start-date-info, .start-date-info,
.end-date-info { .end-date-info {
@include float(left); @include float(left);
...@@ -86,9 +139,35 @@ ...@@ -86,9 +139,35 @@
} }
.codes { .codes {
@include float(left);
width: 100%;
.heading { .heading {
margin-bottom: 10px; margin-bottom: 10px;
} }
} }
} }
} }
.catalog_buttons {
margin-top: 10px;
margin-bottom: 34px;
}
#catalogModal {
.modal-dialog {
margin-top: 100px;
width: 75%;
}
.modal-header {
border: none;
.modal-title {
text-align: center;
font-weight: 800;
}
}
}
...@@ -38,10 +38,11 @@ ...@@ -38,10 +38,11 @@
<div class="value"><%= discountValue %></div> <div class="value"><%= discountValue %></div>
</div> </div>
<div class="info-item course-info"> <div class="info-item course-info">
<div class="heading"><%= gettext('Valid for course:') %></div> <div class="heading"><%= gettext('Valid for courses:') %></div>
<div class="value"><%= course_id %> </div>
<span class="pull-right"><%= certificate_type %></span> <div class="info-item catalog-query">
</div> <div class="heading"><%= gettext('Catalog query:') %></div>
<div class="value"><%= coupon['catalog_query'] %></div>
</div> </div>
<div class="info-item date-info"> <div class="info-item date-info">
<div class="start-date-info"> <div class="start-date-info">
...@@ -53,6 +54,11 @@ ...@@ -53,6 +54,11 @@
<div class="value"><%= endDateTime %></div> <div class="value"><%= endDateTime %></div>
</div> </div>
</div> </div>
<div class="info-item seat-types">
<div class="heading"><%= gettext('Seat types:') %></div>
<div class="value"><%= coupon['course_seat_types'] %></div>
<div class="catalog_buttons"></div>
</div>
<div class="info-item usage-limitations"> <div class="info-item usage-limitations">
<div class="heading"><%= gettext('Usage Limitations:') %></div> <div class="heading"><%= gettext('Usage Limitations:') %></div>
<div class="value"><%= usage %></div> <div class="value"><%= usage %></div>
......
...@@ -102,6 +102,15 @@ ...@@ -102,6 +102,15 @@
<div class="fields col-md-6"> <div class="fields col-md-6">
<div class="form-group"> <div class="form-group">
<div class="form-inline catalog-type">
<input id="single_course" type="radio" name="catalog_type" value="Single course">
<label for="single_course"><%= gettext('Single course') %></label>
<input id="multiple_courses" type="radio" name="catalog_type" value="Multiple courses">
<label for="multiple_courses"><%= gettext('Multiple courses') %></label>
</div>
<p class="help-block"></p>
</div>
<div class="form-group">
<label for="course_id"><%= gettext('Course ID') %> *</label> <label for="course_id"><%= gettext('Course ID') %> *</label>
<input type="text" class="form-control" name="course_id"> <input type="text" class="form-control" name="course_id">
<p class="help-block"></p> <p class="help-block"></p>
...@@ -111,6 +120,22 @@ ...@@ -111,6 +120,22 @@
<select class="form-control" name="seat_type"></select> <select class="form-control" name="seat_type"></select>
<p class="help-block"></p> <p class="help-block"></p>
</div> </div>
<div class="form-group catalog-query">
<label for="catalog_query"><%= gettext('Valid for:') %> *</label>
<textarea class="form-control" name="catalog_query" rows="10"></textarea>
<p class="help-block"></p>
</div>
<div class="form-group course-seat-types">
<label for="course_seat_types"><%= gettext('Seat Types:') %></label>
<div class="checkboxes">
<input id="verified" type="checkbox" name="course_seat_types" value="verified">
<label for="verified"><%= gettext('Verified') %></label>
<input id="professional" type="checkbox" name="course_seat_types" value="professional">
<label for="professional"><%= gettext('Professional') %></label>
<p class="help-block"></p>
</div>
<div class="catalog_buttons"></div>
</div>
<div class="form-group"> <div class="form-group">
<label for="note"><%= gettext('Note') %></label> <label for="note"><%= gettext('Note') %></label>
<input type="text" class="form-control" name="note" maxlength="100"> <input type="text" class="form-control" name="note" maxlength="100">
......
<button name="preview_catalog" data-toggle="modal" data-target="#catalogModal">
<%= gettext('Preview') %>
</button>
<div class="modal fade" id="catalogModal" tabindex="-1" role="dialog" aria-labelledby="catalogModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="catalogModalLabel"><%= gettext('Catalog Details') %></h4>
</div>
<div class="modal-body">
<table id="coursesTable" class="copy copy-base table table-striped table-bordered" cellspacing="0">
</table>
</div>
</div>
</div>
</div>
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