Commit c5746e6d by chrisndodge

Merge pull request #6071 from edx/muhhshoaib/WL-135-add-expiration-dates-to-Coupon-codes

Add expiration dates to Coupon Codes
parents 17c2af23 10f8d8c0
...@@ -4,6 +4,8 @@ Unit tests for instructor.api methods. ...@@ -4,6 +4,8 @@ Unit tests for instructor.api methods.
""" """
import datetime import datetime
import ddt import ddt
import random
import pytz
import io import io
import json import json
import os import os
...@@ -61,7 +63,7 @@ from .test_tools import msk_from_problem_urlname ...@@ -61,7 +63,7 @@ from .test_tools import msk_from_problem_urlname
from ..views.tools import get_extended_due from ..views.tools import get_extended_due
EXPECTED_CSV_HEADER = '"code","course_id","company_name","created_by","redeemed_by","invoice_id","purchaser","customer_reference_number","internal_reference"' EXPECTED_CSV_HEADER = '"code","course_id","company_name","created_by","redeemed_by","invoice_id","purchaser","customer_reference_number","internal_reference"'
EXPECTED_COUPON_CSV_HEADER = '"course_id","percentage_discount","code_redeemed_count","description"' EXPECTED_COUPON_CSV_HEADER = '"code","course_id","percentage_discount","code_redeemed_count","description"'
# ddt data for test cases involving reports # ddt data for test cases involving reports
REPORTS_DATA = ( REPORTS_DATA = (
...@@ -3331,8 +3333,27 @@ class TestCourseRegistrationCodes(ModuleStoreTestCase): ...@@ -3331,8 +3333,27 @@ class TestCourseRegistrationCodes(ModuleStoreTestCase):
) )
coupon.save() coupon.save()
#now create coupons with the expiration dates
for i in range(5):
coupon = Coupon(
code='coupon{0}'.format(i), description='test_description', course_id=self.course.id,
percentage_discount='{0}'.format(i), created_by=self.instructor, is_active=True,
expiration_date=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2)
)
coupon.save()
response = self.client.get(get_coupon_code_url) response = self.client.get(get_coupon_code_url)
self.assertEqual(response.status_code, 200, response.content) self.assertEqual(response.status_code, 200, response.content)
# filter all the coupons
for coupon in Coupon.objects.all():
self.assertIn('"{code}","{course_id}","{discount}","0","{description}","{expiration_date}"'.format(
code=coupon.code,
course_id=coupon.course_id,
discount=coupon.percentage_discount,
description=coupon.description,
expiration_date=coupon.display_expiry_date
), response.content)
self.assertEqual(response['Content-Type'], 'text/csv') self.assertEqual(response['Content-Type'], 'text/csv')
body = response.content.replace('\r', '') body = response.content.replace('\r', '')
self.assertTrue(body.startswith(EXPECTED_COUPON_CSV_HEADER)) self.assertTrue(body.startswith(EXPECTED_COUPON_CSV_HEADER))
......
...@@ -3,6 +3,8 @@ Unit tests for Ecommerce feature flag in new instructor dashboard. ...@@ -3,6 +3,8 @@ Unit tests for Ecommerce feature flag in new instructor dashboard.
""" """
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
import datetime
import pytz
from django.test.utils import override_settings from django.test.utils import override_settings
from mock import patch from mock import patch
...@@ -144,13 +146,26 @@ class TestECommerceDashboardViews(ModuleStoreTestCase): ...@@ -144,13 +146,26 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
""" """
# URL for add_coupon # URL for add_coupon
add_coupon_url = reverse('add_coupon', kwargs={'course_id': self.course.id.to_deprecated_string()}) add_coupon_url = reverse('add_coupon', kwargs={'course_id': self.course.id.to_deprecated_string()})
expiration_date = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2)
data = { data = {
'code': 'A2314', 'course_id': self.course.id.to_deprecated_string(), 'code': 'A2314', 'course_id': self.course.id.to_deprecated_string(),
'description': 'ADSADASDSAD', 'created_by': self.instructor, 'discount': 5 'description': 'ADSADASDSAD', 'created_by': self.instructor, 'discount': 5,
'expiration_date': '{month}/{day}/{year}'.format(month=expiration_date.month, day=expiration_date.day, year=expiration_date.year)
} }
response = self.client.post(add_coupon_url, data) response = self.client.post(add_coupon_url, data)
self.assertTrue("coupon with the coupon code ({code}) added successfully".format(code=data['code']) in response.content) self.assertTrue("coupon with the coupon code ({code}) added successfully".format(code=data['code']) in response.content)
#now add the coupon with the wrong value in the expiration_date
# server will through the ValueError Exception in the expiration_date field
data = {
'code': '213454', 'course_id': self.course.id.to_deprecated_string(),
'description': 'ADSADASDSAD', 'created_by': self.instructor, 'discount': 5,
'expiration_date': expiration_date.strftime('"%d/%m/%Y')
}
response = self.client.post(add_coupon_url, data)
self.assertTrue("Please enter the date in this format i-e month/day/year" in response.content)
data = { data = {
'code': 'A2314', 'course_id': self.course.id.to_deprecated_string(), 'code': 'A2314', 'course_id': self.course.id.to_deprecated_string(),
'description': 'asdsasda', 'created_by': self.instructor, 'discount': 99 'description': 'asdsasda', 'created_by': self.instructor, 'discount': 99
...@@ -221,13 +236,15 @@ class TestECommerceDashboardViews(ModuleStoreTestCase): ...@@ -221,13 +236,15 @@ class TestECommerceDashboardViews(ModuleStoreTestCase):
""" """
coupon = Coupon( coupon = Coupon(
code='AS452', description='asdsadsa', course_id=self.course.id.to_deprecated_string(), code='AS452', description='asdsadsa', course_id=self.course.id.to_deprecated_string(),
percentage_discount=10, created_by=self.instructor percentage_discount=10, created_by=self.instructor,
expiration_date=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2)
) )
coupon.save() coupon.save()
# URL for edit_coupon_info # URL for edit_coupon_info
edit_url = reverse('get_coupon_info', kwargs={'course_id': self.course.id.to_deprecated_string()}) edit_url = reverse('get_coupon_info', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.post(edit_url, {'id': coupon.id}) response = self.client.post(edit_url, {'id': coupon.id})
self.assertTrue('coupon with the coupon id ({coupon_id}) updated successfully'.format(coupon_id=coupon.id) in response.content) self.assertTrue('coupon with the coupon id ({coupon_id}) updated successfully'.format(coupon_id=coupon.id) in response.content)
self.assertIn(coupon.display_expiry_date, response.content)
response = self.client.post(edit_url, {'id': 444444}) response = self.client.post(edit_url, {'id': 444444})
self.assertTrue('coupon with the coupon id ({coupon_id}) DoesNotExist'.format(coupon_id=444444) in response.content) self.assertTrue('coupon with the coupon id ({coupon_id}) DoesNotExist'.format(coupon_id=444444) in response.content)
......
...@@ -18,6 +18,7 @@ from django.views.decorators.cache import cache_control ...@@ -18,6 +18,7 @@ from django.views.decorators.cache import cache_control
from django.core.exceptions import ValidationError, PermissionDenied from django.core.exceptions import ValidationError, PermissionDenied
from django.core.mail.message import EmailMessage from django.core.mail.message import EmailMessage
from django.db import IntegrityError from django.db import IntegrityError
from django.db.models import Q
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.validators import validate_email from django.core.validators import validate_email
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
...@@ -28,6 +29,8 @@ import random ...@@ -28,6 +29,8 @@ import random
import unicodecsv import unicodecsv
import urllib import urllib
from util.file import store_uploaded_file, course_and_time_based_filename_generator, FileValidationException, UniversalNewlineIterator from util.file import store_uploaded_file, course_and_time_based_filename_generator, FileValidationException, UniversalNewlineIterator
import datetime
import pytz
from util.json_request import JsonResponse from util.json_request import JsonResponse
from instructor.views.instructor_task_helpers import extract_email_features, extract_task_features from instructor.views.instructor_task_helpers import extract_email_features, extract_task_features
...@@ -1007,9 +1010,14 @@ def get_coupon_codes(request, course_id): # pylint: disable=unused-argument ...@@ -1007,9 +1010,14 @@ def get_coupon_codes(request, course_id): # pylint: disable=unused-argument
Respond with csv which contains a summary of all Active Coupons. Respond with csv which contains a summary of all Active Coupons.
""" """
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
active_coupons = Coupon.objects.filter(course_id=course_id, is_active=True) active_coupons = Coupon.objects.filter(
Q(course_id=course_id),
Q(is_active=True),
Q(expiration_date__gt=datetime.datetime.now(pytz.UTC)) |
Q(expiration_date__isnull=True)
)
query_features = [ query_features = [
'course_id', 'percentage_discount', 'code_redeemed_count', 'description' 'code', 'course_id', 'percentage_discount', 'code_redeemed_count', 'description', 'expiration_date'
] ]
coupons_list = instructor_analytics.basic.coupon_codes_features(query_features, active_coupons) coupons_list = instructor_analytics.basic.coupon_codes_features(query_features, active_coupons)
header, data_rows = instructor_analytics.csvs.format_dictlist(coupons_list, query_features) header, data_rows = instructor_analytics.csvs.format_dictlist(coupons_list, query_features)
......
...@@ -10,7 +10,8 @@ from util.json_request import JsonResponse ...@@ -10,7 +10,8 @@ from util.json_request import JsonResponse
from django.http import HttpResponse, HttpResponseNotFound from django.http import HttpResponse, HttpResponseNotFound
from shoppingcart.models import Coupon, CourseRegistrationCode from shoppingcart.models import Coupon, CourseRegistrationCode
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
import datetime
import pytz
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -79,9 +80,22 @@ def add_coupon(request, course_id): # pylint: disable=unused-argument ...@@ -79,9 +80,22 @@ def add_coupon(request, course_id): # pylint: disable=unused-argument
return JsonResponse({ return JsonResponse({
'message': _("Please Enter the Coupon Discount Value Less than or Equal to 100") 'message': _("Please Enter the Coupon Discount Value Less than or Equal to 100")
}, status=400) # status code 400: Bad Request }, status=400) # status code 400: Bad Request
expiration_date = None
if request.POST.get('expiration_date'):
expiration_date = request.POST.get('expiration_date')
try:
expiration_date = datetime.datetime.strptime(expiration_date, "%m/%d/%Y").replace(tzinfo=pytz.UTC) + datetime.timedelta(days=1)
except ValueError:
return JsonResponse({
'message': _("Please enter the date in this format i-e month/day/year")
}, status=400) # status code 400: Bad Request
coupon = Coupon( coupon = Coupon(
code=code, description=description, course_id=course_id, code=code, description=description,
percentage_discount=discount, created_by_id=request.user.id course_id=course_id,
percentage_discount=discount,
created_by_id=request.user.id,
expiration_date=expiration_date
) )
coupon.save() coupon.save()
return JsonResponse( return JsonResponse(
...@@ -143,10 +157,12 @@ def get_coupon_info(request, course_id): # pylint: disable=unused-argument ...@@ -143,10 +157,12 @@ def get_coupon_info(request, course_id): # pylint: disable=unused-argument
'message': _("coupon with the coupon id ({coupon_id}) is already inactive").format(coupon_id=coupon_id) 'message': _("coupon with the coupon id ({coupon_id}) is already inactive").format(coupon_id=coupon_id)
}, status=400) # status code 400: Bad Request }, status=400) # status code 400: Bad Request
expiry_date = coupon.display_expiry_date
return JsonResponse({ return JsonResponse({
'coupon_code': coupon.code, 'coupon_code': coupon.code,
'coupon_description': coupon.description, 'coupon_description': coupon.description,
'coupon_course_id': coupon.course_id.to_deprecated_string(), 'coupon_course_id': coupon.course_id.to_deprecated_string(),
'coupon_discount': coupon.percentage_discount, 'coupon_discount': coupon.percentage_discount,
'expiry_date': expiry_date,
'message': _('coupon with the coupon id ({coupon_id}) updated successfully').format(coupon_id=coupon_id) 'message': _('coupon with the coupon id ({coupon_id}) updated successfully').format(coupon_id=coupon_id)
}) # status code 200: OK by default }) # status code 200: OK by default
...@@ -30,7 +30,7 @@ SALE_ORDER_FEATURES = ('id', 'company_name', 'company_contact_name', 'company_co ...@@ -30,7 +30,7 @@ SALE_ORDER_FEATURES = ('id', 'company_name', 'company_contact_name', 'company_co
AVAILABLE_FEATURES = STUDENT_FEATURES + PROFILE_FEATURES AVAILABLE_FEATURES = STUDENT_FEATURES + PROFILE_FEATURES
COURSE_REGISTRATION_FEATURES = ('code', 'course_id', 'created_by', 'created_at') COURSE_REGISTRATION_FEATURES = ('code', 'course_id', 'created_by', 'created_at')
COUPON_FEATURES = ('course_id', 'percentage_discount', 'description') COUPON_FEATURES = ('code', 'course_id', 'percentage_discount', 'description', 'expiration_date')
def sale_order_record_features(course_id, features): def sale_order_record_features(course_id, features):
...@@ -228,6 +228,7 @@ def coupon_codes_features(features, coupons_list): ...@@ -228,6 +228,7 @@ def coupon_codes_features(features, coupons_list):
# codes csv. In the case of active and generated registration codes the redeemed_by value will be None. # codes csv. In the case of active and generated registration codes the redeemed_by value will be None.
# They have not been redeemed yet # They have not been redeemed yet
coupon_dict['expiration_date'] = coupon.display_expiry_date
coupon_dict['course_id'] = coupon_dict['course_id'].to_deprecated_string() coupon_dict['course_id'] = coupon_dict['course_id'].to_deprecated_string()
return coupon_dict return coupon_dict
return [extract_coupon(coupon, features) for coupon in coupons_list] return [extract_coupon(coupon, features) for coupon in coupons_list]
......
...@@ -22,6 +22,10 @@ from courseware.tests.factories import InstructorFactory ...@@ -22,6 +22,10 @@ from courseware.tests.factories import InstructorFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
import datetime
from django.db.models import Q
import pytz
class TestAnalyticsBasic(ModuleStoreTestCase): class TestAnalyticsBasic(ModuleStoreTestCase):
""" Test basic analytics functions. """ """ Test basic analytics functions. """
...@@ -303,7 +307,7 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase): ...@@ -303,7 +307,7 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase):
def test_coupon_codes_features(self): def test_coupon_codes_features(self):
query_features = [ query_features = [
'course_id', 'percentage_discount', 'code_redeemed_count', 'description' 'course_id', 'percentage_discount', 'code_redeemed_count', 'description', 'expiration_date'
] ]
for i in range(10): for i in range(10):
coupon = Coupon( coupon = Coupon(
...@@ -314,13 +318,29 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase): ...@@ -314,13 +318,29 @@ class TestCourseRegistrationCodeAnalyticsBasic(ModuleStoreTestCase):
is_active=True is_active=True
) )
coupon.save() coupon.save()
active_coupons = Coupon.objects.filter(course_id=self.course.id, is_active=True) #now create coupons with the expiration dates
for i in range(5):
coupon = Coupon(
code='coupon{0}'.format(i), description='test_description', course_id=self.course.id,
percentage_discount='{0}'.format(i), created_by=self.instructor, is_active=True,
expiration_date=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2)
)
coupon.save()
active_coupons = Coupon.objects.filter(
Q(course_id=self.course.id),
Q(is_active=True),
Q(expiration_date__gt=datetime.datetime.now(pytz.UTC)) |
Q(expiration_date__isnull=True)
)
active_coupons_list = coupon_codes_features(query_features, active_coupons) active_coupons_list = coupon_codes_features(query_features, active_coupons)
self.assertEqual(len(active_coupons_list), len(active_coupons)) self.assertEqual(len(active_coupons_list), len(active_coupons))
for active_coupon in active_coupons_list: for active_coupon in active_coupons_list:
self.assertEqual(set(active_coupon.keys()), set(query_features)) self.assertEqual(set(active_coupon.keys()), set(query_features))
self.assertIn(active_coupon['percentage_discount'], [coupon.percentage_discount for coupon in active_coupons]) self.assertIn(active_coupon['percentage_discount'], [coupon.percentage_discount for coupon in active_coupons])
self.assertIn(active_coupon['description'], [coupon.description for coupon in active_coupons]) self.assertIn(active_coupon['description'], [coupon.description for coupon in active_coupons])
if active_coupon['expiration_date']:
self.assertIn(active_coupon['expiration_date'], [coupon.display_expiry_date for coupon in active_coupons])
self.assertIn( self.assertIn(
active_coupon['course_id'], active_coupon['course_id'],
[coupon.course_id.to_deprecated_string() for coupon in active_coupons] [coupon.course_id.to_deprecated_string() for coupon in active_coupons]
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
from collections import namedtuple from collections import namedtuple
from datetime import datetime from datetime import datetime
from datetime import timedelta
from decimal import Decimal from decimal import Decimal
import analytics import analytics
import pytz import pytz
...@@ -803,12 +804,20 @@ class Coupon(models.Model): ...@@ -803,12 +804,20 @@ class Coupon(models.Model):
created_by = models.ForeignKey(User) created_by = models.ForeignKey(User)
created_at = models.DateTimeField(default=datetime.now(pytz.utc)) created_at = models.DateTimeField(default=datetime.now(pytz.utc))
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
expiration_date = models.DateTimeField(null=True, blank=True)
def __unicode__(self): def __unicode__(self):
return "[Coupon] code: {} course: {}".format(self.code, self.course_id) return "[Coupon] code: {} course: {}".format(self.code, self.course_id)
objects = SoftDeleteCouponManager() objects = SoftDeleteCouponManager()
@property
def display_expiry_date(self):
"""
return the coupon expiration date in the readable format
"""
return (self.expiration_date - timedelta(days=1)).strftime("%B %d, %Y") if self.expiration_date else None
class CouponRedemption(models.Model): class CouponRedemption(models.Model):
""" """
......
...@@ -369,7 +369,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ...@@ -369,7 +369,7 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.coupon_code}) resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.coupon_code})
self.assertEqual(resp.status_code, 404) self.assertEqual(resp.status_code, 404)
self.assertIn("Coupon '{0}' is not valid for any course in the shopping cart.".format(self.coupon_code), resp.content) self.assertIn("Discount does not exist against code '{0}'.".format(self.coupon_code), resp.content)
def test_course_does_not_exist_in_cart_against_valid_reg_code(self): def test_course_does_not_exist_in_cart_against_valid_reg_code(self):
course_key = self.course_key.to_deprecated_string() + 'testing' course_key = self.course_key.to_deprecated_string() + 'testing'
......
...@@ -2,6 +2,7 @@ import logging ...@@ -2,6 +2,7 @@ import logging
import datetime import datetime
import decimal import decimal
import pytz import pytz
from django.db.models import Q
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.http import ( from django.http import (
...@@ -258,7 +259,12 @@ def use_code(request): ...@@ -258,7 +259,12 @@ def use_code(request):
Registration Code Redemption page. Registration Code Redemption page.
""" """
code = request.POST["code"] code = request.POST["code"]
coupons = Coupon.objects.filter(code=code, is_active=True) coupons = Coupon.objects.filter(
Q(code=code),
Q(is_active=True),
Q(expiration_date__gt=datetime.datetime.now(pytz.UTC)) |
Q(expiration_date__isnull=True)
)
if not coupons: if not coupons:
# If no coupons then we check that code against course registration code # If no coupons then we check that code against course registration code
try: try:
...@@ -423,9 +429,8 @@ def use_coupon_code(coupons, user): ...@@ -423,9 +429,8 @@ def use_coupon_code(coupons, user):
return HttpResponseBadRequest(_("Only one coupon redemption is allowed against an order")) return HttpResponseBadRequest(_("Only one coupon redemption is allowed against an order"))
if not is_redemption_applied: if not is_redemption_applied:
log.warning("Course item does not exist for coupon '{code}'".format(code=coupons[0].code)) log.warning("Discount does not exist against code '{code}'.".format(code=coupons[0].code))
return HttpResponseNotFound( return HttpResponseNotFound(_("Discount does not exist against code '{code}'.".format(code=coupons[0].code)))
_("Coupon '{code}' is not valid for any course in the shopping cart.".format(code=coupons[0].code)))
return HttpResponse( return HttpResponse(
json.dumps({'response': 'success', 'coupon_code_applied': True}), json.dumps({'response': 'success', 'coupon_code_applied': True}),
......
var edx = edx || {};
(function(Backbone, $, _) {
'use strict';
edx.instructor_dashboard = edx.instructor_dashboard || {};
edx.instructor_dashboard.ecommerce = {};
edx.instructor_dashboard.ecommerce.ExpiryCouponView = Backbone.View.extend({
el: 'li#add-coupon-modal-field-expiry',
events: {
'click input[type="checkbox"]': 'clicked'
},
initialize: function() {
$('li#add-coupon-modal-field-expiry input[name="expiration_date"]').hide();
_.bindAll(this, 'clicked');
},
clicked: function (event) {
if (event.currentTarget.checked) {
this.$el.find('#coupon_expiration_date').show();
this.$el.find('#coupon_expiration_date').focus();
}
else {
this.$el.find('#coupon_expiration_date').hide();
}
}
});
$(function() {
$( "#coupon_expiration_date" ).datepicker({
minDate: 0
});
var view = new edx.instructor_dashboard.ecommerce.ExpiryCouponView();
});
}).call(this, Backbone, $, _);
\ No newline at end of file
define(['backbone', 'jquery', 'js/instructor_dashboard/ecommerce', 'js/common_helpers/template_helpers'],
function (Backbone, $, ExpiryCouponView, TemplateHelpers) {
'use strict';
var expiryCouponView, createExpiryCoupon;
describe("edx.instructor_dashboard.ecommerce.ExpiryCouponView", function() {
beforeEach(function() {
setFixtures('<li class="field full-width" id="add-coupon-modal-field-expiry"><input id="expiry-check" type="checkbox"/><label for="expiry-check"></label><input type="text" id="coupon_expiration_date" class="field" name="expiration_date" aria-required="true"/></li>')
expiryCouponView = new ExpiryCouponView();
});
it("is defined", function () {
expect(expiryCouponView).toBeDefined();
});
it("triggers the callback when the checkbox is clicked", function () {
var target = expiryCouponView.$el.find('input[type="checkbox"]');
spyOn(expiryCouponView, 'clicked');
expiryCouponView.delegateEvents();
target.click();
expect(expiryCouponView.clicked).toHaveBeenCalled();
});
it("shows the input field when the checkbox is checked", function () {
var target = expiryCouponView.$el.find('input[type="checkbox"]');
target.attr("checked","checked");
target.click();
expect(expiryCouponView.$el.find('#coupon_expiration_date')).toHaveAttr('style','display: inline;');
});
it("hides the input field when the checkbox is unchecked", function () {
var target = expiryCouponView.$el.find('input[type="checkbox"]');
expect(expiryCouponView.$el.find('#coupon_expiration_date')).toHaveAttr('style','display: none;');
});
});
});
...@@ -270,6 +270,10 @@ ...@@ -270,6 +270,10 @@
}, },
// Backbone classes loaded explicitly until they are converted to use RequireJS // Backbone classes loaded explicitly until they are converted to use RequireJS
'js/instructor_dashboard/ecommerce': {
exports: 'edx.instructor_dashboard.ecommerce.ExpiryCouponView',
deps: ['backbone', 'jquery', 'underscore']
},
'js/models/cohort': { 'js/models/cohort': {
exports: 'CohortModel', exports: 'CohortModel',
deps: ['backbone'] deps: ['backbone']
...@@ -497,6 +501,7 @@ ...@@ -497,6 +501,7 @@
'lms/include/js/spec/views/file_uploader_spec.js', 'lms/include/js/spec/views/file_uploader_spec.js',
'lms/include/js/spec/dashboard/donation.js', 'lms/include/js/spec/dashboard/donation.js',
'lms/include/js/spec/shoppingcart/shoppingcart_spec.js', 'lms/include/js/spec/shoppingcart/shoppingcart_spec.js',
'lms/include/js/spec/instructor_dashboard/ecommerce_spec.js',
'lms/include/js/spec/student_account/account_spec.js', 'lms/include/js/spec/student_account/account_spec.js',
'lms/include/js/spec/student_account/access_spec.js', 'lms/include/js/spec/student_account/access_spec.js',
'lms/include/js/spec/student_account/login_spec.js', 'lms/include/js/spec/student_account/login_spec.js',
......
...@@ -12,6 +12,9 @@ ...@@ -12,6 +12,9 @@
} }
} }
#ui-datepicker-div{z-index: 12000 !important; width: 16.5em !important}
.instructor-dashboard-wrapper-2 { .instructor-dashboard-wrapper-2 {
position: relative; position: relative;
// display: table; // display: table;
...@@ -1325,12 +1328,13 @@ input[name="subject"] { ...@@ -1325,12 +1328,13 @@ input[name="subject"] {
th { th {
text-align: left; text-align: left;
border-bottom: 1px solid $border-color-1; border-bottom: 1px solid $border-color-1;
font-size: 16px;
&.c_code { &.c_code {
width: 170px; width: 110px;
} }
&.c_count { &.c_count {
width: 85px; width: 60px;
} }
&.c_course_id { &.c_course_id {
width: 320px; width: 320px;
...@@ -1339,11 +1343,14 @@ input[name="subject"] { ...@@ -1339,11 +1343,14 @@ input[name="subject"] {
&.c_discount { &.c_discount {
width: 90px; width: 90px;
} }
&.c_expiry {
width: 150px;
}
&.c_action { &.c_action {
width: 89px; width: 60px;
} }
&.c_dsc{ &.c_dsc{
width: 260px; width: 200px;
word-wrap: break-word; word-wrap: break-word;
} }
} }
...@@ -1361,6 +1368,16 @@ input[name="subject"] { ...@@ -1361,6 +1368,16 @@ input[name="subject"] {
} }
} }
} }
// in_active coupon rows style
.expired_coupon{
background: #FEEFB3 !important;
color: rgba(51,51,51,0.2);
border-bottom: 1px solid #fff;
td:nth-child(3) {
text-decoration: line-through;
}
}
// coupon items style // coupon items style
.coupons-items { .coupons-items {
...@@ -1368,6 +1385,7 @@ input[name="subject"] { ...@@ -1368,6 +1385,7 @@ input[name="subject"] {
padding: ($baseline/2) 0; padding: ($baseline/2) 0;
position: relative; position: relative;
line-height: normal; line-height: normal;
font-size: 14px;
span.old-price{ span.old-price{
left: -75px; left: -75px;
position: relative; position: relative;
...@@ -1613,7 +1631,15 @@ input[name="subject"] { ...@@ -1613,7 +1631,15 @@ input[name="subject"] {
} }
} }
} }
#add-coupon-modal{
ol.list-input{
li{
input[type="checkbox"]#expiry-check , input[type="checkbox"]#expiry-check + label {display: inline-block; width: auto;margin-top: 10px;}
&.full-width{width: 100%;}
input#coupon_expiration_date{width: 278px;display: inline-block;float: right;}
}
}
}
} }
.profile-distribution-widget { .profile-distribution-widget {
......
...@@ -49,6 +49,12 @@ ...@@ -49,6 +49,12 @@
<input class="field readonly" id="coupon_course_id" type="text" name="course_id" value="${section_data['course_id'] | h}" <input class="field readonly" id="coupon_course_id" type="text" name="course_id" value="${section_data['course_id'] | h}"
readonly aria-required="true"/> readonly aria-required="true"/>
</li> </li>
<li class="field full-width" id="add-coupon-modal-field-expiry">
<input id="expiry-check" type="checkbox" value="true" />
<label for="expiry-check">${_('Add expiration date')}</label>
<input type="text" id="coupon_expiration_date" value="" class="field" name="expiration_date"
aria-required="true"/>
</li>
</ol> </ol>
</fieldset> </fieldset>
......
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<%! from datetime import datetime, timedelta %>
<%! import pytz %>
<%page args="section_data"/> <%page args="section_data"/>
<%include file="add_coupon_modal.html" args="section_data=section_data" /> <%include file="add_coupon_modal.html" args="section_data=section_data" />
<%include file="edit_coupon_modal.html" args="section_data=section_data" /> <%include file="edit_coupon_modal.html" args="section_data=section_data" />
...@@ -95,6 +97,7 @@ ...@@ -95,6 +97,7 @@
<tr class="coupons-headings"> <tr class="coupons-headings">
<th class="c_code">${_("Code")}</th> <th class="c_code">${_("Code")}</th>
<th class="c_dsc">${_("Description")}</th> <th class="c_dsc">${_("Description")}</th>
<th class="c_expiry">${_("Expiry Date")}</th>
<th class="c_discount">${_("Discount (%)")}</th> <th class="c_discount">${_("Discount (%)")}</th>
<th class="c_count">${_("Redeem Count")}</th> <th class="c_count">${_("Redeem Count")}</th>
<th class="c_action">${_("Actions")}</th> <th class="c_action">${_("Actions")}</th>
...@@ -102,14 +105,21 @@ ...@@ -102,14 +105,21 @@
</thead> </thead>
<tbody> <tbody>
%for coupon in section_data['coupons']: %for coupon in section_data['coupons']:
<% current_date = datetime.now(pytz.UTC) %>
<% coupon_expiry_date = coupon.expiration_date %>
%if coupon.is_active == False: %if coupon.is_active == False:
<tr class="coupons-items inactive_coupon"> <tr class="coupons-items inactive_coupon">
%else: %elif coupon_expiry_date is not None and current_date >= coupon_expiry_date:
<tr class="coupons-items expired_coupon">
%else:
<tr class="coupons-items"> <tr class="coupons-items">
%endif %endif
<td>${coupon.code}</td> <td>${_('{code}').format(code=coupon.code)}</td>
<td>${coupon.description}</td> <td>${_('{description}').format(description=coupon.description)}</td>
<td>${coupon.percentage_discount}</td> <td>
${coupon.display_expiry_date}
</td>
<td>${_('{discount}').format(discount=coupon.percentage_discount)}</td>
<td>${ coupon.couponredemption_set.filter(order__status='purchased').count() }</td> <td>${ coupon.couponredemption_set.filter(order__status='purchased').count() }</td>
<td><a data-item-id="${coupon.id}" class='remove_coupon' href='#'>[x]</a><a href="#edit-modal" data-item-id="${coupon.id}" class="edit-right">${_('Edit')}</a></td> <td><a data-item-id="${coupon.id}" class='remove_coupon' href='#'>[x]</a><a href="#edit-modal" data-item-id="${coupon.id}" class="edit-right">${_('Edit')}</a></td>
</tr> </tr>
...@@ -184,7 +194,7 @@ ...@@ -184,7 +194,7 @@
} }
if($('#invoice_number').val() == "") { if($('#invoice_number').val() == "") {
$('#error-msg').attr('class','error-msgs') $('#error-msg').attr('class','error-msgs')
$('#error-msg').html("${_("Invoice number should not be empty.")}").show(); $('#error-msg').html("${_('Invoice number should not be empty.')}").show();
return return
} }
$.ajax({ $.ajax({
...@@ -224,6 +234,12 @@ ...@@ -224,6 +234,12 @@
$('input#edit_coupon_discount').val(data.coupon_discount); $('input#edit_coupon_discount').val(data.coupon_discount);
$('textarea#edit_coupon_description').val(data.coupon_description); $('textarea#edit_coupon_description').val(data.coupon_description);
$('input#edit_coupon_course_id').val(data.coupon_course_id); $('input#edit_coupon_course_id').val(data.coupon_course_id);
if (data.expiry_date) {
$('input#edit_coupon_expiration_date').val(data.expiry_date);
}
else {
$('input#edit_coupon_expiration_date').val("${_('Never Expires')}");
}
$('#edit-modal-trigger').click(); $('#edit-modal-trigger').click();
}, },
error: function(jqXHR, textStatus, errorThrown) { error: function(jqXHR, textStatus, errorThrown) {
...@@ -459,6 +475,7 @@ ...@@ -459,6 +475,7 @@
var coupon_discount = $.trim($('#coupon_discount').val()); var coupon_discount = $.trim($('#coupon_discount').val());
var course_id = $.trim($('#coupon_course_id').val()); var course_id = $.trim($('#coupon_course_id').val());
var description = $.trim($('#coupon_description').val()); var description = $.trim($('#coupon_description').val());
var expiration_date = $.trim($('#coupon_expiration_date').val());
// Check if empty of not // Check if empty of not
if (code === '') { if (code === '') {
...@@ -485,7 +502,8 @@ ...@@ -485,7 +502,8 @@
"code" : code, "code" : code,
"discount": coupon_discount, "discount": coupon_discount,
"course_id": course_id, "course_id": course_id,
"description": description "description": description,
"expiration_date": expiration_date
}, },
url: "${section_data['ajax_add_coupon']}", url: "${section_data['ajax_add_coupon']}",
success: function (data) { success: function (data) {
......
...@@ -49,6 +49,12 @@ ...@@ -49,6 +49,12 @@
<input class="field readonly" id="edit_coupon_course_id" type="text" name="course_id" value="" <input class="field readonly" id="edit_coupon_course_id" type="text" name="course_id" value=""
readonly aria-required="true"/> readonly aria-required="true"/>
</li> </li>
<li class="field" id="edit-coupon-modal-field-expiration-date">
<label for="edit_coupon_expiration_date">${_("Expiration Date")}</label>
<input class="field readonly" id="edit_coupon_expiration_date" type="text" name="expiration_date" value=""
readonly aria-required="true"/>
</li>
</ol> </ol>
</fieldset> </fieldset>
......
...@@ -53,6 +53,7 @@ ...@@ -53,6 +53,7 @@
<%static:js group='application'/> <%static:js group='application'/>
## Backbone classes declared explicitly until RequireJS is supported ## Backbone classes declared explicitly until RequireJS is supported
<script type="text/javascript" src="${static.url('js/instructor_dashboard/ecommerce.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/notification.js')}"></script> <script type="text/javascript" src="${static.url('js/models/notification.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/notification.js')}"></script> <script type="text/javascript" src="${static.url('js/views/notification.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/file_uploader.js')}"></script> <script type="text/javascript" src="${static.url('js/views/file_uploader.js')}"></script>
......
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