WL-135 added the migration for the expiration_date field in the coupons model
added the functionality of adding the expiration date, and also handled in the shopping cart when the user try to add the expired coupon code in the shopping cart added expiration date in the coupons list in the ecommerce page added the unit tests and jasmine tests fix the quality issues and rebased with master regenerated the migration file with a different number i18n and verified te course mode changes suggested by stephan
This commit is contained in:
committed by
Muhammad Shoaib
parent
30b572b1b1
commit
10f8d8c097
@@ -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
|
|||||||
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):
|
|||||||
)
|
)
|
||||||
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.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
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):
|
|||||||
"""
|
"""
|
||||||
# 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):
|
|||||||
"""
|
"""
|
||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
'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
|
|||||||
|
|
||||||
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):
|
|||||||
# 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
|
|||||||
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):
|
|||||||
|
|
||||||
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):
|
|||||||
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]
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import datetime
|
||||||
|
from south.db import db
|
||||||
|
from south.v2 import SchemaMigration
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(SchemaMigration):
|
||||||
|
|
||||||
|
def forwards(self, orm):
|
||||||
|
# Adding field 'Coupon.expiration_date'
|
||||||
|
db.add_column('shoppingcart_coupon', 'expiration_date',
|
||||||
|
self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True),
|
||||||
|
keep_default=False)
|
||||||
|
|
||||||
|
|
||||||
|
def backwards(self, orm):
|
||||||
|
# Deleting field 'Coupon.expiration_date'
|
||||||
|
db.delete_column('shoppingcart_coupon', 'expiration_date')
|
||||||
|
|
||||||
|
|
||||||
|
models = {
|
||||||
|
'auth.group': {
|
||||||
|
'Meta': {'object_name': 'Group'},
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||||
|
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||||
|
},
|
||||||
|
'auth.permission': {
|
||||||
|
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||||
|
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||||
|
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||||
|
},
|
||||||
|
'auth.user': {
|
||||||
|
'Meta': {'object_name': 'User'},
|
||||||
|
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||||
|
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||||
|
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||||
|
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||||
|
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||||
|
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||||
|
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||||
|
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||||
|
},
|
||||||
|
'contenttypes.contenttype': {
|
||||||
|
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||||
|
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||||
|
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||||
|
},
|
||||||
|
'shoppingcart.certificateitem': {
|
||||||
|
'Meta': {'object_name': 'CertificateItem', '_ormbases': ['shoppingcart.OrderItem']},
|
||||||
|
'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']"}),
|
||||||
|
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}),
|
||||||
|
'mode': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
|
||||||
|
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
|
||||||
|
},
|
||||||
|
'shoppingcart.coupon': {
|
||||||
|
'Meta': {'object_name': 'Coupon'},
|
||||||
|
'code': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||||
|
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255'}),
|
||||||
|
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 1, 6, 0, 0)'}),
|
||||||
|
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||||
|
'description': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||||
|
'expiration_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'percentage_discount': ('django.db.models.fields.IntegerField', [], {'default': '0'})
|
||||||
|
},
|
||||||
|
'shoppingcart.couponredemption': {
|
||||||
|
'Meta': {'object_name': 'CouponRedemption'},
|
||||||
|
'coupon': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Coupon']"}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'shoppingcart.courseregcodeitem': {
|
||||||
|
'Meta': {'object_name': 'CourseRegCodeItem', '_ormbases': ['shoppingcart.OrderItem']},
|
||||||
|
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}),
|
||||||
|
'mode': ('django.db.models.fields.SlugField', [], {'default': "'honor'", 'max_length': '50'}),
|
||||||
|
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
|
||||||
|
},
|
||||||
|
'shoppingcart.courseregcodeitemannotation': {
|
||||||
|
'Meta': {'object_name': 'CourseRegCodeItemAnnotation'},
|
||||||
|
'annotation': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||||
|
'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||||
|
},
|
||||||
|
'shoppingcart.courseregistrationcode': {
|
||||||
|
'Meta': {'object_name': 'CourseRegistrationCode'},
|
||||||
|
'code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
|
||||||
|
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||||
|
'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 1, 6, 0, 0)'}),
|
||||||
|
'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_by_user'", 'to': "orm['auth.User']"}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'invoice': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Invoice']", 'null': 'True'}),
|
||||||
|
'order': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'purchase_order'", 'null': 'True', 'to': "orm['shoppingcart.Order']"})
|
||||||
|
},
|
||||||
|
'shoppingcart.donation': {
|
||||||
|
'Meta': {'object_name': 'Donation', '_ormbases': ['shoppingcart.OrderItem']},
|
||||||
|
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||||
|
'donation_type': ('django.db.models.fields.CharField', [], {'default': "'general'", 'max_length': '32'}),
|
||||||
|
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
|
||||||
|
},
|
||||||
|
'shoppingcart.donationconfiguration': {
|
||||||
|
'Meta': {'object_name': 'DonationConfiguration'},
|
||||||
|
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||||
|
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
|
||||||
|
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||||
|
},
|
||||||
|
'shoppingcart.invoice': {
|
||||||
|
'Meta': {'object_name': 'Invoice'},
|
||||||
|
'address_line_1': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||||
|
'address_line_2': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||||
|
'address_line_3': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||||
|
'city': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||||
|
'company_contact_email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||||
|
'company_contact_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||||
|
'company_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||||
|
'country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
|
||||||
|
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||||
|
'customer_reference_number': ('django.db.models.fields.CharField', [], {'max_length': '63', 'null': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'internal_reference': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||||
|
'is_valid': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'recipient_email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||||
|
'recipient_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||||
|
'state': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
|
||||||
|
'total_amount': ('django.db.models.fields.FloatField', [], {}),
|
||||||
|
'zip': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True'})
|
||||||
|
},
|
||||||
|
'shoppingcart.order': {
|
||||||
|
'Meta': {'object_name': 'Order'},
|
||||||
|
'bill_to_cardtype': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
|
||||||
|
'bill_to_ccnum': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}),
|
||||||
|
'bill_to_city': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||||
|
'bill_to_country': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||||
|
'bill_to_first': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||||
|
'bill_to_last': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
|
||||||
|
'bill_to_postalcode': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}),
|
||||||
|
'bill_to_state': ('django.db.models.fields.CharField', [], {'max_length': '8', 'blank': 'True'}),
|
||||||
|
'bill_to_street1': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
|
||||||
|
'bill_to_street2': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
|
||||||
|
'company_contact_email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||||
|
'company_contact_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||||
|
'company_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||||
|
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
|
||||||
|
'customer_reference_number': ('django.db.models.fields.CharField', [], {'max_length': '63', 'null': 'True', 'blank': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'order_type': ('django.db.models.fields.CharField', [], {'default': "'personal'", 'max_length': '32'}),
|
||||||
|
'processor_reply_dump': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||||
|
'purchase_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||||
|
'recipient_email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||||
|
'recipient_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||||
|
'refunded_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
|
||||||
|
'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32'}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'shoppingcart.orderitem': {
|
||||||
|
'Meta': {'object_name': 'OrderItem'},
|
||||||
|
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||||
|
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
|
||||||
|
'fulfilled_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'line_desc': ('django.db.models.fields.CharField', [], {'default': "'Misc. Item'", 'max_length': '1024'}),
|
||||||
|
'list_price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '30', 'decimal_places': '2'}),
|
||||||
|
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
|
||||||
|
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']"}),
|
||||||
|
'qty': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
|
||||||
|
'refund_requested_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||||
|
'report_comments': ('django.db.models.fields.TextField', [], {'default': "''"}),
|
||||||
|
'service_fee': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}),
|
||||||
|
'status': ('django.db.models.fields.CharField', [], {'default': "'cart'", 'max_length': '32', 'db_index': 'True'}),
|
||||||
|
'unit_cost': ('django.db.models.fields.DecimalField', [], {'default': '0.0', 'max_digits': '30', 'decimal_places': '2'}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||||
|
},
|
||||||
|
'shoppingcart.paidcourseregistration': {
|
||||||
|
'Meta': {'object_name': 'PaidCourseRegistration', '_ormbases': ['shoppingcart.OrderItem']},
|
||||||
|
'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']", 'null': 'True'}),
|
||||||
|
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '128', 'db_index': 'True'}),
|
||||||
|
'mode': ('django.db.models.fields.SlugField', [], {'default': "'honor'", 'max_length': '50'}),
|
||||||
|
'orderitem_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['shoppingcart.OrderItem']", 'unique': 'True', 'primary_key': 'True'})
|
||||||
|
},
|
||||||
|
'shoppingcart.paidcourseregistrationannotation': {
|
||||||
|
'Meta': {'object_name': 'PaidCourseRegistrationAnnotation'},
|
||||||
|
'annotation': ('django.db.models.fields.TextField', [], {'null': 'True'}),
|
||||||
|
'course_id': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||||
|
},
|
||||||
|
'shoppingcart.registrationcoderedemption': {
|
||||||
|
'Meta': {'object_name': 'RegistrationCodeRedemption'},
|
||||||
|
'course_enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']", 'null': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'order': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.Order']", 'null': 'True'}),
|
||||||
|
'redeemed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 1, 6, 0, 0)', 'null': 'True'}),
|
||||||
|
'redeemed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||||
|
'registration_code': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['shoppingcart.CourseRegistrationCode']"})
|
||||||
|
},
|
||||||
|
'student.courseenrollment': {
|
||||||
|
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
|
||||||
|
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||||
|
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||||
|
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||||
|
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||||
|
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}),
|
||||||
|
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
complete_apps = ['shoppingcart']
|
||||||
@@ -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):
|
|||||||
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):
|
|||||||
|
|
||||||
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
|
|||||||
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):
|
|||||||
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):
|
|||||||
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}),
|
||||||
|
|||||||
35
lms/static/js/instructor_dashboard/ecommerce.js
Normal file
35
lms/static/js/instructor_dashboard/ecommerce.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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, $, _);
|
||||||
36
lms/static/js/spec/instructor_dashboard/ecommerce_spec.js
Normal file
36
lms/static/js/spec/instructor_dashboard/ecommerce_spec.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 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 @@
|
|||||||
'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 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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"] {
|
|||||||
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"] {
|
|||||||
&.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"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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"] {
|
|||||||
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"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#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 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<%! 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 @@
|
|||||||
<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 @@
|
|||||||
</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 @@
|
|||||||
}
|
}
|
||||||
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 @@
|
|||||||
$('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 @@
|
|||||||
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 @@
|
|||||||
"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 @@
|
|||||||
<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 @@
|
|||||||
<%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>
|
||||||
|
|||||||
Reference in New Issue
Block a user