Commit af9f2d4a by Marko Jevtić Committed by GitHub

Merge pull request #824 from edx/mjevtic/patch-update-coupon

Enable patch editing of the coupons
parents 5621f060 0356d9c4
...@@ -80,6 +80,27 @@ class CouponViewSetTest(CouponMixin, CourseCatalogTestMixin, TestCase): ...@@ -80,6 +80,27 @@ class CouponViewSetTest(CouponMixin, CourseCatalogTestMixin, TestCase):
site.siteconfiguration = site_configuration site.siteconfiguration = site_configuration
return site return site
def test_retrieve_invoice_data(self):
request_data = {
'invoice_discount_type': Invoice.PERCENTAGE,
'invoice_discount_value': 50,
'invoice_number': 'INV-00055',
'invoice_payment_date': datetime.datetime(2016, 1, 1, tzinfo=pytz.UTC).isoformat(),
'invoice_type': Invoice.PREPAID,
'tax_deducted_source': None
}
invoice_data = CouponViewSet().retrieve_invoice_data(request_data)
self.assertDictEqual(invoice_data, {
'discount_type': request_data['invoice_discount_type'],
'discount_value': request_data['invoice_discount_value'],
'number': request_data['invoice_number'],
'payment_date': request_data['invoice_payment_date'],
'type': request_data['invoice_type'],
'tax_deducted_source': request_data['tax_deducted_source']
})
@ddt.data( @ddt.data(
(Voucher.ONCE_PER_CUSTOMER, 2, 2), (Voucher.ONCE_PER_CUSTOMER, 2, 2),
(Voucher.SINGLE_USE, 2, None) (Voucher.SINGLE_USE, 2, None)
...@@ -526,6 +547,20 @@ class CouponViewSetFunctionalTest(CouponMixin, CourseCatalogTestMixin, CatalogPr ...@@ -526,6 +547,20 @@ 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_update_invoice_data(self):
coupon = Product.objects.get(title='Test coupon')
invoice = Invoice.objects.get(order__basket__lines__product=coupon)
self.assertEqual(invoice.discount_type, Invoice.PERCENTAGE)
CouponViewSet().update_invoice_data(
coupon=coupon,
data={
'invoice_discount_type': Invoice.FIXED
}
)
invoice = Invoice.objects.get(order__basket__lines__product=coupon)
self.assertEqual(invoice.discount_type, Invoice.FIXED)
@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."""
......
...@@ -43,6 +43,21 @@ Voucher = get_model('voucher', 'Voucher') ...@@ -43,6 +43,21 @@ Voucher = get_model('voucher', 'Voucher')
CATALOG_QUERY = 'catalog_query' CATALOG_QUERY = 'catalog_query'
CLIENT = 'client' CLIENT = 'client'
COURSE_SEAT_TYPES = 'course_seat_types' COURSE_SEAT_TYPES = 'course_seat_types'
INVOICE_DISCOUNT_TYPE = 'invoice_discount_type'
INVOICE_DISCOUNT_VALUE = 'invoice_discount_value'
INVOICE_NUMBER = 'invoice_number'
INVOICE_PAYMENT_DATE = 'invoice_payment_date'
INVOICE_TYPE = 'invoice_type'
TAX_DEDUCTED_SOURCE = 'tax_deducted_source'
UPDATEABLE_INVOICE_FIELDS = [
INVOICE_DISCOUNT_TYPE,
INVOICE_DISCOUNT_VALUE,
INVOICE_NUMBER,
INVOICE_PAYMENT_DATE,
INVOICE_TYPE,
TAX_DEDUCTED_SOURCE,
]
UPDATABLE_RANGE_FIELDS = [ UPDATABLE_RANGE_FIELDS = [
CATALOG_QUERY, CATALOG_QUERY,
...@@ -64,14 +79,17 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet): ...@@ -64,14 +79,17 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet):
def retrieve_invoice_data(self, request_data): def retrieve_invoice_data(self, request_data):
""" Retrieve the invoice information from the request data. """ """ Retrieve the invoice information from the request data. """
return { invoice_data = {}
'number': request_data.get('invoice_number'),
'type': request_data.get('invoice_type'), for field in UPDATEABLE_INVOICE_FIELDS:
'payment_date': request_data.get('invoice_payment_date'), self.create_update_data_dict(
'discount_type': request_data.get('invoice_discount_type'), request_data=request_data,
'discount_value': request_data.get('invoice_discount_value'), request_data_key=field,
'tax_deducted_source': request_data.get('tax_deducted_source'), update_dict=invoice_data,
} update_dict_key=field.replace('invoice_', '')
)
return invoice_data
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
"""Adds coupon to the user's basket. """Adds coupon to the user's basket.
...@@ -373,8 +391,8 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet): ...@@ -373,8 +391,8 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet):
update_dict (dict): Dictionary containing the coupon update data update_dict (dict): Dictionary containing the coupon update data
update_dict_key (str): Update data dictionary key update_dict_key (str): Update data dictionary key
""" """
value = request_data.get(request_data_key, '') if request_data_key in request_data:
if value: value = request_data.get(request_data_key)
update_dict[update_dict_key] = prepare_course_seat_types(value) \ update_dict[update_dict_key] = prepare_course_seat_types(value) \
if update_dict_key == COURSE_SEAT_TYPES else value if update_dict_key == COURSE_SEAT_TYPES else value
...@@ -442,9 +460,10 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet): ...@@ -442,9 +460,10 @@ class CouponViewSet(EdxOrderPlacementMixin, viewsets.ModelViewSet):
data (dict): The request's data from which the invoice data is retrieved data (dict): The request's data from which the invoice data is retrieved
and used for the updated. and used for the updated.
""" """
invoice = Invoice.objects.filter(order__basket__lines__product=coupon) invoice_data = self.retrieve_invoice_data(data)
update_data = self.retrieve_invoice_data(data)
invoice.update(**update_data) if invoice_data:
Invoice.objects.filter(order__basket__lines__product=coupon).update(**invoice_data)
def destroy(self, request, pk): # pylint: disable=unused-argument def destroy(self, request, pk): # pylint: disable=unused-argument
try: try:
......
...@@ -139,6 +139,7 @@ define([ ...@@ -139,6 +139,7 @@ define([
}, },
initialize: function () { initialize: function () {
this.on('change:categories', this.updateCategory, this);
this.on('change:voucher_type', this.changeVoucherType, this); this.on('change:voucher_type', this.changeVoucherType, this);
this.on('change:vouchers', this.updateVoucherData); this.on('change:vouchers', this.updateVoucherData);
this.on('change:seats', this.updateSeatData); this.on('change:seats', this.updateSeatData);
...@@ -178,6 +179,12 @@ define([ ...@@ -178,6 +179,12 @@ define([
return course_id ? course_id.value : ''; return course_id ? course_id.value : '';
}, },
updateCategory: function() {
var categoryID = this.get('categories')[0].id;
this.set('category', categoryID);
this.set('category_ids', [categoryID]);
},
updateSeatData: function () { updateSeatData: function () {
var seat_data, var seat_data,
seats = this.get('seats'); seats = this.get('seats');
...@@ -230,27 +237,37 @@ define([ ...@@ -230,27 +237,37 @@ define([
'invoice_payment_date': invoice.payment_date, 'invoice_payment_date': invoice.payment_date,
'tax_deducted_source': invoice.tax_deducted_source, 'tax_deducted_source': invoice.tax_deducted_source,
'tax_deduction': tax_deducted, 'tax_deduction': tax_deducted,
}); });
}, },
save: function (options) { save: function (attributes, options) {
_.defaults(options || (options = {}), { _.defaults(options || (options = {}), {
// The API requires a CSRF token for all POST requests using session authentication. // The API requires a CSRF token for all POST requests using session authentication.
headers: {'X-CSRFToken': Cookies.get('ecommerce_csrftoken')}, headers: {'X-CSRFToken': Cookies.get('ecommerce_csrftoken')},
contentType: 'application/json' contentType: 'application/json'
}); });
this.set('start_date', moment.utc(this.get('start_date'))); if (!options.patch){
this.set('end_date', moment.utc(this.get('end_date'))); this.set('start_date', moment.utc(this.get('start_date')));
this.set('category_ids', [this.get('category')]); this.set('end_date', moment.utc(this.get('end_date')));
if (this.get('coupon_type') === 'Enrollment code') {
this.set('benefit_type', 'Percentage');
this.set('benefit_value', 100);
}
options.data = JSON.stringify(this.toJSON());
} else {
if (_.has(attributes, 'start_date')) {
attributes.start_date = moment.utc(attributes.start_date);
}
if (this.get('coupon_type') === 'Enrollment code') { if (_.has(attributes, 'end_date')) {
this.set('benefit_type', 'Percentage'); attributes.end_date = moment.utc(attributes.end_date);
this.set('benefit_value', 100); }
} }
options.data = JSON.stringify(this.toJSON()); return this._super(attributes, options);
return this._super(null, options);
} }
}); });
} }
......
...@@ -145,6 +145,21 @@ define([ ...@@ -145,6 +145,21 @@ define([
ajaxData = JSON.parse(args[0].data); ajaxData = JSON.parse(args[0].data);
expect(ajaxData.quantity).toEqual(1); expect(ajaxData.quantity).toEqual(1);
}); });
it('should format start and end date if they are patch updated', function () {
var model = Coupon.findOrCreate(discountCodeData, {parse: true});
spyOn(moment, 'utc');
model.save(
{
start_date: '2015-11-11T00:00:00Z',
end_date: '2016-11-11T00:00:00Z'
},
{patch: true}
);
expect(moment.utc).toHaveBeenCalledWith('2015-11-11T00:00:00Z');
expect(moment.utc).toHaveBeenCalledWith('2016-11-11T00:00:00Z');
});
}); });
}); });
......
...@@ -73,7 +73,7 @@ define([ ...@@ -73,7 +73,7 @@ define([
expect(view.$el.find('[name=code]').val()).toEqual(model.get('code')); expect(view.$el.find('[name=code]').val()).toEqual(model.get('code'));
}); });
}); });
describe('Coupon with invoice data', function() { describe('Coupon with invoice data', function() {
beforeEach(function() { beforeEach(function() {
model = Coupon.findOrCreate(invoice_coupon_data, {parse: true}); model = Coupon.findOrCreate(invoice_coupon_data, {parse: true});
...@@ -96,6 +96,17 @@ define([ ...@@ -96,6 +96,17 @@ define([
expect(view.$el.find('[name=tax_deducted_source_value]').val()) expect(view.$el.find('[name=tax_deducted_source_value]').val())
.toEqual(model.get('tax_deducted_source')); .toEqual(model.get('tax_deducted_source'));
}); });
it('should patch save the model when form is in editing mode and has editable attributes', function () {
var formView = view.formView;
spyOn(formView.model, 'save');
spyOn(formView.model, 'isValid').and.returnValue(true);
expect(formView.modelServerState).toEqual(model.pick(formView.editableAttributes));
formView.model.set('title', 'Test Title');
formView.submit($.Event('click'));
expect(model.save).toHaveBeenCalled();
});
}); });
}); });
} }
......
...@@ -139,7 +139,6 @@ define([ ...@@ -139,7 +139,6 @@ define([
errorObj = { responseJSON: { error: 'An error occurred while saving the data.' }}; errorObj = { responseJSON: { error: 'An error occurred while saving the data.' }};
testErrorResponse(); testErrorResponse();
}); });
}); });
} }
); );
...@@ -79,6 +79,10 @@ define([ ...@@ -79,6 +79,10 @@ define([
}, },
setOptions: { setOptions: {
validate: true validate: true
},
onSet: function(val) {
this.model.set('category_ids', [val]);
return val;
} }
}, },
'input[name=title]': { 'input[name=title]': {
...@@ -226,6 +230,27 @@ define([ ...@@ -226,6 +230,27 @@ define([
this.editing = options.editing || false; this.editing = options.editing || false;
this.hiddenClass = 'hidden'; this.hiddenClass = 'hidden';
if (this.editing) {
this.editableAttributes = [
'benefit_value',
'catalog_query',
'category_ids',
'client',
'course_seat_types',
'end_date',
'invoice_discount_type',
'invoice_discount_value',
'invoice_number',
'invoice_payment_date',
'invoice_type',
'note',
'price',
'start_date',
'tax_deducted_source',
'title',
];
}
this.dynamic_catalog_view = new DynamicCatalogView({ this.dynamic_catalog_view = new DynamicCatalogView({
'query': this.model.get('catalog_query'), 'query': this.model.get('catalog_query'),
'seat_types': this.model.get('course_seat_types') 'seat_types': this.model.get('course_seat_types')
...@@ -303,7 +328,7 @@ define([ ...@@ -303,7 +328,7 @@ define([
this.formGroup('[name=code]').addClass(this.hiddenClass); this.formGroup('[name=code]').addClass(this.hiddenClass);
} }
}, },
toggleInvoiceFields: function () { toggleInvoiceFields: function () {
var invoice_type = this.$('[name=invoice_type]:checked').val(), var invoice_type = this.$('[name=invoice_type]:checked').val(),
prepaid_fields = [ prepaid_fields = [
...@@ -523,7 +548,6 @@ define([ ...@@ -523,7 +548,6 @@ define([
this.$alerts = this.$('.alerts'); this.$alerts = this.$('.alerts');
if (this.editing) { if (this.editing) {
this.$('select[name=category]').val(this.model.get('categories')[0].id).trigger('change');
this.disableNonEditableFields(); this.disableNonEditableFields();
this.toggleCouponTypeField(); this.toggleCouponTypeField();
this.toggleVoucherTypeField(); this.toggleVoucherTypeField();
......
...@@ -28,6 +28,10 @@ define([ ...@@ -28,6 +28,10 @@ define([
initialize: function () { initialize: function () {
this.alertViews = []; this.alertViews = [];
if (this.editing && _.has(this, 'editableAttributes')) {
this.modelServerState = this.model.pick(this.editableAttributes);
}
// Enable validation // Enable validation
Utils.bindValidation(this); Utils.bindValidation(this);
}, },
...@@ -149,7 +153,9 @@ define([ ...@@ -149,7 +153,9 @@ define([
self = this, self = this,
courseId = $('input[name=id]').val(), courseId = $('input[name=id]').val(),
btnSavingContent = '<i class="fa fa-spinner fa-spin" aria-hidden="true"></i> ' + btnSavingContent = '<i class="fa fa-spinner fa-spin" aria-hidden="true"></i> ' +
gettext('Saving...'); gettext('Saving...'),
onSaveComplete,
onSaveError;
e.preventDefault(); e.preventDefault();
...@@ -174,33 +180,54 @@ define([ ...@@ -174,33 +180,54 @@ define([
// Disable all buttons by setting the attribute (for <button>) and class (for <a>) // Disable all buttons by setting the attribute (for <button>) and class (for <a>)
$buttons.attr('disabled', 'disabled').addClass('disabled'); $buttons.attr('disabled', 'disabled').addClass('disabled');
this.model.save({ onSaveComplete = function () {
complete: function () { // Restore the button text
// Restore the button text $submitButton.text(btnDefaultText);
$submitButton.text(btnDefaultText);
// Re-enable the buttons // Re-enable the buttons
$buttons.removeAttr('disabled').removeClass('disabled'); $buttons.removeAttr('disabled').removeClass('disabled');
}, };
success: this.saveSuccess.bind(this),
error: function (model, response) { onSaveError = function (model, response) {
var message = gettext('An error occurred while saving the data.'); var message = gettext('An error occurred while saving the data.');
if (response.responseJSON && response.responseJSON.error) {
message = response.responseJSON.error;
// Log the error to the console for debugging purposes
console.error(message);
} else {
// Log the error to the console for debugging purposes
console.error(response.responseText);
}
self.clearAlerts(); if (response.responseJSON && response.responseJSON.error) {
self.renderAlert('danger', message); message = response.responseJSON.error;
self.$el.animate({scrollTop: 0}, 'slow');
// Log the error to the console for debugging purposes
console.error(message);
} else {
// Log the error to the console for debugging purposes
console.error(response.responseText);
} }
});
self.clearAlerts();
self.renderAlert('danger', message);
self.$el.animate({scrollTop: 0}, 'slow');
};
if (this.editing && _.has(this, 'editableAttributes')) {
var editableAttributes = this.model.pick(this.editableAttributes),
changedAttributes = _.omit(editableAttributes, function(value, key) {
return value === this.modelServerState[key];
}, this);
this.model.save(
changedAttributes,
{
complete: onSaveComplete,
error: onSaveError,
patch: true,
success: this.saveSuccess.bind(this)
}
);
} else {
this.model.save({
complete: onSaveComplete,
success: this.saveSuccess.bind(this),
error: onSaveError
});
}
return this; return this;
} }
......
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