Commit c9f1d6d0 by Zubair Afzal

Revert "ENT-110: Add Catalog selection option to Coupon create/edit form"

parent 104f6147
......@@ -149,7 +149,7 @@ class CouponMixin(object):
def create_coupon(self, benefit_type=Benefit.PERCENTAGE, benefit_value=100, catalog=None,
catalog_query=None, client=None, code='', course_seat_types=None, email_domains=None,
max_uses=None, note=None, partner=None, price=100, quantity=5, title='Test coupon',
voucher_type=Voucher.SINGLE_USE, course_catalog=None):
voucher_type=Voucher.SINGLE_USE):
"""Helper method for creating a coupon.
Arguments:
......@@ -157,18 +157,12 @@ class CouponMixin(object):
benefit_value(int): The voucher benefit value
catalog(Catalog): Catalog of courses for which the coupon applies
catalog_query(str): Course query string
client (BusinessClient): Optional business client object
code(str): Custom coupon code
course_catalog (int): Course catalog id from Catalog Service
course_seat_types(str): A string of comma-separated list of seat types
email_domains(str): A comma seperated list of email domains
max_uses (int): Number of Voucher max uses
note (str): Coupon note.
partner(Partner): Partner used for creating a catalog
price(int): Price of the coupon
quantity (int): Number of vouchers to be created and associated with the coupon
title(str): Title of the coupon
voucher_type (str): Voucher type
Returns:
coupon (Coupon)
......@@ -190,7 +184,6 @@ class CouponMixin(object):
catalog_query=catalog_query,
category=self.category,
code=code,
course_catalog=course_catalog,
course_seat_types=course_seat_types,
email_domains=email_domains,
end_datetime=datetime.datetime(2020, 1, 1),
......
import json
import httpretty
from django.conf import settings
from django.core.cache import cache
class CourseCatalogServiceMockMixin(object):
"""
Mocks for the Open edX service 'Course Catalog Service' responses.
"""
COURSE_DISCOVERY_CATALOGS_URL = '{}catalogs/'.format(
settings.COURSE_CATALOG_API_URL,
)
def setUp(self):
super(CourseCatalogServiceMockMixin, self).setUp()
cache.clear()
def mock_course_discovery_api_for_catalog_by_resource_id(self):
"""
Helper function to register course catalog API endpoint for a
single catalog with its resource id.
"""
catalog_id = 1
course_discovery_api_response = {
'count': 1,
'next': None,
'previous': None,
'results': [
{
'id': catalog_id,
'name': 'Catalog {}'.format(catalog_id),
'query': 'title: *',
'courses_count': 0,
'viewers': []
}
]
}
course_discovery_api_response_json = json.dumps(course_discovery_api_response)
single_catalog_uri = '{}{}/'.format(self.COURSE_DISCOVERY_CATALOGS_URL, catalog_id)
httpretty.register_uri(
method=httpretty.GET,
uri=single_catalog_uri,
body=course_discovery_api_response_json,
content_type='application/json'
)
def mock_course_discovery_api_for_catalogs(self, catalog_name_list):
"""
Helper function to register course catalog API endpoint for a
single catalog or multiple catalogs response.
"""
mocked_results = []
for catalog_index, catalog_name in enumerate(catalog_name_list):
catalog_id = catalog_index + 1
mocked_results.append(
{
'id': catalog_id,
'name': catalog_name,
'query': 'title: *',
'courses_count': 0,
'viewers': []
}
)
course_discovery_api_response = {
'count': len(catalog_name_list),
'next': None,
'previous': None,
'results': mocked_results
}
course_discovery_api_response_json = json.dumps(course_discovery_api_response)
httpretty.register_uri(
method=httpretty.GET,
uri=self.COURSE_DISCOVERY_CATALOGS_URL,
body=course_discovery_api_response_json,
content_type='application/json'
)
def mock_course_discovery_api_for_paginated_catalogs(self, catalog_name_list):
"""
Helper function to register course catalog API endpoint for multiple
catalogs with paginated response.
"""
mocked_api_responses = []
for catalog_index, catalog_name in enumerate(catalog_name_list):
catalog_id = catalog_index + 1
mocked_result = {
'id': catalog_id,
'name': catalog_name,
'query': 'title: *',
'courses_count': 0,
'viewers': []
}
next_page_url = None
if catalog_id < len(catalog_name_list):
# Not a last page so there will be more catalogs for another page
next_page_url = '{}?limit=1&offset={}'.format(
self.COURSE_DISCOVERY_CATALOGS_URL,
catalog_id
)
previous_page_url = None
if catalog_index != 0:
# Not a first page so there will always be catalogs on previous page
previous_page_url = '{}?limit=1&offset={}'.format(
self.COURSE_DISCOVERY_CATALOGS_URL,
catalog_index
)
course_discovery_api_paginated_response = {
'count': len(catalog_name_list),
'next': next_page_url,
'previous': previous_page_url,
'results': [mocked_result]
}
course_discovery_api_paginated_response_json = json.dumps(course_discovery_api_paginated_response)
mocked_api_responses.append(
httpretty.Response(body=course_discovery_api_paginated_response_json, content_type='application/json')
)
httpretty.register_uri(
method=httpretty.GET,
uri=self.COURSE_DISCOVERY_CATALOGS_URL,
responses=mocked_api_responses
)
def mock_course_discovery_api_for_failure(self):
"""
Helper function to register course catalog API endpoint for a
failure.
"""
httpretty.register_uri(
method=httpretty.GET,
uri=self.COURSE_DISCOVERY_CATALOGS_URL,
responses=[
httpretty.Response(body='Clunk', content_type='application/json', status_code=500)
]
)
import hashlib
import ddt
from django.core.cache import cache
import httpretty
from django.core.cache import cache
from ecommerce.core.constants import ENROLLMENT_CODE_SWITCH
from ecommerce.core.tests import toggle_switch
from ecommerce.core.tests.decorators import mock_course_catalog_api_client
from ecommerce.coupons.tests.mixins import CourseCatalogMockMixin
from ecommerce.courses.models import Course
from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.courses.tests.mixins import CourseCatalogServiceMockMixin
from ecommerce.courses.utils import (
get_certificate_type_display_value, get_course_info_from_catalog, mode_for_seat, get_course_catalogs
get_certificate_type_display_value, get_course_info_from_catalog, mode_for_seat
)
from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin
from ecommerce.tests.testcases import TestCase
......@@ -74,110 +74,3 @@ class UtilsTests(CourseCatalogTestMixin, CourseCatalogMockMixin, TestCase):
def test_cert_display_assertion(self):
""" Verify assertion for invalid cert type """
self.assertRaises(ValueError, lambda: get_certificate_type_display_value('junk'))
@ddt.ddt
@httpretty.activate
class GetCourseCatalogUtilTests(CourseCatalogServiceMockMixin, TestCase):
def tearDown(self):
# Reset HTTPretty state (clean up registered urls and request history)
httpretty.reset()
def _assert_num_requests(self, count):
"""
DRY helper for verifying request counts.
"""
self.assertEqual(len(httpretty.httpretty.latest_requests), count)
def _assert_get_course_catalogs(self, catalog_name_list):
"""
Helper method to validate the response from the method
"get_course_catalogs".
"""
cache_key = '{}.catalog.api.data'.format(self.request.site.domain)
cache_key = hashlib.md5(cache_key).hexdigest()
cached_course_catalogs = cache.get(cache_key)
self.assertIsNone(cached_course_catalogs)
response = get_course_catalogs(self.request.site)
self.assertEqual(len(response), len(catalog_name_list))
for catalog_index, catalog in enumerate(response):
self.assertEqual(catalog['name'], catalog_name_list[catalog_index])
cached_course = cache.get(cache_key)
self.assertEqual(cached_course, response)
@mock_course_catalog_api_client
def test_get_course_catalogs_for_single_catalog_with_id(self):
"""
Verify that method "get_course_catalogs" returns proper response for a
single catalog by its id.
"""
self.mock_course_discovery_api_for_catalog_by_resource_id()
catalog_id = 1
cache_key = '{}.catalog.api.data.{}'.format(self.request.site.domain, catalog_id)
cache_key = hashlib.md5(cache_key).hexdigest()
cached_course_catalog = cache.get(cache_key)
self.assertIsNone(cached_course_catalog)
response = get_course_catalogs(self.request.site, catalog_id)
self.assertEqual(response['count'], 1)
self.assertEqual(response['results'][0]['name'], 'Catalog {}'.format(catalog_id))
cached_course = cache.get(cache_key)
self.assertEqual(cached_course, response)
# Verify the API was actually hit (not the cache)
self._assert_num_requests(1)
@mock_course_catalog_api_client
@ddt.data(
['Catalog 1'],
['Catalog 1', 'Catalog 2'],
)
def test_get_course_catalogs_for_single_page_api_response(self, catalog_name_list):
"""
Verify that method "get_course_catalogs" returns proper response for
single page Course Discovery API response and uses cache to return data
in case of same API request.
"""
self.mock_course_discovery_api_for_catalogs(catalog_name_list)
self._assert_get_course_catalogs(catalog_name_list)
# Verify the API was hit once
self._assert_num_requests(1)
# Now fetch the catalogs again and there should be no more actual call
# to Course Discovery API as the data will be fetched from the cache
get_course_catalogs(self.request.site)
self._assert_num_requests(1)
@mock_course_catalog_api_client
def test_get_course_catalogs_for_paginated_api_response(self):
"""
Verify that method "get_course_catalogs" returns all catalogs for
paginated Course Discovery API response for multiple catalogs.
"""
catalog_name_list = ['Catalog 1', 'Catalog 2', 'Catalog 3']
self.mock_course_discovery_api_for_paginated_catalogs(catalog_name_list)
self._assert_get_course_catalogs(catalog_name_list)
# Verify the API was hit for each catalog page
self._assert_num_requests(len(catalog_name_list))
@mock_course_catalog_api_client
def test_get_course_catalogs_for_failure(self):
"""
Verify that method "get_course_catalogs" raises exception in case
the Course Discovery API fails to return data.
"""
self.mock_course_discovery_api_for_failure()
with self.assertRaises(Exception):
get_course_catalogs(self.request.site)
import hashlib
from urlparse import parse_qs, urlparse
from django.conf import settings
from django.core.cache import cache
......@@ -34,67 +33,6 @@ def get_course_info_from_catalog(site, course_key):
return course_run
def get_course_catalogs(site, resource_id=None):
"""
Get details related to course catalogs from Catalog Service.
Arguments:
site (Site): Site object containing Site Configuration data
resource_id (int or str): Identifies a specific resource to be retrieved
Returns:
dict: Course catalogs received from Course Catalog API
"""
resource = 'catalogs'
base_cache_key = '{}.catalog.api.data'.format(site.domain)
cache_key = '{}.{}'.format(base_cache_key, resource_id) if resource_id else base_cache_key
cache_key = hashlib.md5(cache_key).hexdigest()
cached = cache.get(cache_key)
if cached:
return cached
api = site.siteconfiguration.course_catalog_api_client
endpoint = getattr(api, resource)
response = endpoint(resource_id).get()
if resource_id:
results = response
else:
results = traverse_pagination(response, endpoint)
cache.set(cache_key, results, settings.COURSES_API_CACHE_TIMEOUT)
return results
def traverse_pagination(response, endpoint):
"""
Traverse a paginated API response.
Extracts and concatenates "results" (list of dict) returned by DRF-powered
APIs.
Arguments:
response (Dict): Current response dict from service API
endpoint (slumber Resource object): slumber Resource object from edx-rest-api-client
Returns:
list of dict.
"""
results = response.get('results', [])
next_page = response.get('next')
while next_page:
querystring = parse_qs(urlparse(next_page).query, keep_blank_values=True)
response = endpoint.get(**querystring)
results += response.get('results', [])
next_page = response.get('next')
return results
def get_certificate_type_display_value(certificate_type):
display_values = {
'audit': _('Audit'),
......
......@@ -531,7 +531,6 @@ class CouponSerializer(ProductPaymentInfoMixin, serializers.ModelSerializer):
benefit_type = serializers.SerializerMethodField()
benefit_value = serializers.SerializerMethodField()
catalog_query = serializers.SerializerMethodField()
course_catalog = serializers.SerializerMethodField()
category = serializers.SerializerMethodField()
client = serializers.SerializerMethodField()
code = serializers.SerializerMethodField()
......@@ -559,9 +558,6 @@ class CouponSerializer(ProductPaymentInfoMixin, serializers.ModelSerializer):
def get_catalog_query(self, obj):
return retrieve_offer(obj).condition.range.catalog_query
def get_course_catalog(self, obj):
return retrieve_offer(obj).condition.range.course_catalog
def get_category(self, obj):
category = ProductCategory.objects.filter(product=obj).first().category
return CategorySerializer(category).data
......@@ -649,7 +645,7 @@ class CouponSerializer(ProductPaymentInfoMixin, serializers.ModelSerializer):
class Meta(object):
model = Product
fields = (
'benefit_type', 'benefit_value', 'catalog_query', 'course_catalog', 'category',
'benefit_type', 'benefit_value', 'catalog_query', 'category',
'client', 'code', 'code_status', 'coupon_type', 'course_seat_types',
'email_domains', 'end_date', 'id', 'last_edited', 'max_uses',
'note', 'num_uses', 'payment_information', 'price', 'quantity',
......
......@@ -5,28 +5,25 @@ import httpretty
from django.conf import settings
from django.core.urlresolvers import reverse
from django.test import RequestFactory
import mock
from oscar.core.loading import get_model
from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import SlumberBaseException
from ecommerce.core.tests.decorators import mock_course_catalog_api_client
from ecommerce.coupons.tests.mixins import CourseCatalogMockMixin
from ecommerce.courses.tests.mixins import CourseCatalogServiceMockMixin
from ecommerce.extensions.api.serializers import ProductSerializer
from ecommerce.extensions.api.v2.tests.views.mixins import CatalogMixin
from ecommerce.extensions.api.v2.views.catalog import CatalogViewSet
from ecommerce.tests.mixins import ApiMockMixin
from ecommerce.tests.testcases import TestCase
Catalog = get_model('catalogue', 'Catalog')
StockRecord = get_model('partner', 'StockRecord')
@httpretty.activate
@ddt.ddt
class CatalogViewSetTest(CatalogMixin, CourseCatalogMockMixin, CourseCatalogServiceMockMixin, ApiMockMixin, TestCase):
class CatalogViewSetTest(CatalogMixin, CourseCatalogMockMixin, ApiMockMixin, TestCase):
"""Test the Catalog and related products APIs."""
catalog_list_path = reverse('api:v2:catalog-list')
......@@ -133,60 +130,6 @@ class CatalogViewSetTest(CatalogMixin, CourseCatalogMockMixin, CourseCatalogServ
response = CatalogViewSet().preview(request)
self.assertEqual(response.status_code, 400)
@ddt.data(
(
'/api/v2/coupons/course_catalogs/',
['Catalog 1'],
['Catalog 1']
),
(
'/api/v2/coupons/course_catalogs/',
['Clean Catalog', 'ABC Catalog', 'New Catalog', 'Edx Catalog'],
['ABC Catalog', 'Clean Catalog', 'Edx Catalog', 'New Catalog']
),
)
@ddt.unpack
@mock_course_catalog_api_client
def test_course_catalogs_for_single_page_api_response(self, url, catalog_name_list, sorted_catalog_name_list):
"""
Test course catalogs list view "course_catalogs" for valid response
with catalogs in alphabetical order.
"""
self.mock_course_discovery_api_for_catalogs(catalog_name_list)
request = self.prepare_request(url)
response = CatalogViewSet().course_catalogs(request)
self.assertEqual(response.status_code, 200)
# Validate that the catalogs are sorted by name in alphabetical order
self._assert_get_course_catalogs_response_with_order(response, sorted_catalog_name_list)
def _assert_get_course_catalogs_response_with_order(self, response, catalog_name_list):
"""
Helper method to validate the response from the method
"course_catalogs".
"""
response_results = response.data.get('results')
self.assertEqual(len(response_results), len(catalog_name_list))
for catalog_index, catalog in enumerate(response_results):
self.assertEqual(catalog['name'], catalog_name_list[catalog_index])
@mock_course_catalog_api_client
@mock.patch('ecommerce.extensions.api.v2.views.catalog.logger.exception')
def test_get_course_catalogs_for_failure(self, mock_exception):
"""
Verify that the course catalogs list view "course_catalogs" returns
empty results list in case the Course Discovery API fails to return
data.
"""
self.mock_course_discovery_api_for_failure()
request = self.prepare_request('/api/v2/coupons/course_catalogs/')
response = CatalogViewSet().course_catalogs(request)
self.assertTrue(mock_exception.called)
self.assertEqual(response.data.get('results'), [])
class PartnerCatalogViewSetTest(CatalogMixin, TestCase):
def setUp(self):
......
......@@ -73,7 +73,6 @@ class CouponViewSetTest(CouponMixin, CourseCatalogTestMixin, TestCase):
'catalog_query': None,
'course_seat_types': None,
'email_domains': None,
'course_catalog': {'id': '', 'name': ''},
}
def setup_site_configuration(self):
......@@ -239,8 +238,7 @@ class CouponViewSetFunctionalTest(CouponMixin, CourseCatalogTestMixin, CourseCat
'start_datetime': str(now() - datetime.timedelta(days=10)),
'stock_record_ids': [1, 2],
'title': 'Tešt čoupon',
'voucher_type': Voucher.SINGLE_USE,
'course_catalog': {'id': '', 'name': ''},
'voucher_type': Voucher.SINGLE_USE
}
self.response = self.client.post(COUPONS_LINK, json.dumps(self.data), 'application/json')
self.coupon = Product.objects.get(title=self.data['title'])
......
......@@ -13,7 +13,6 @@ from slumber.exceptions import SlumberBaseException
from ecommerce.core.constants import DEFAULT_CATALOG_PAGE_SIZE
from ecommerce.coupons.utils import get_range_catalog_query_results
from ecommerce.extensions.api import serializers
from ecommerce.courses.utils import get_course_catalogs
Catalog = get_model('catalogue', 'Catalog')
......@@ -74,20 +73,3 @@ class CatalogViewSet(NestedViewSetMixin, ReadOnlyModelViewSet):
logger.error('Unable to connect to Course Catalog service.')
return Response(status=status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_400_BAD_REQUEST)
@action(is_for_list=True, methods=['get'])
def course_catalogs(self, request):
"""
Returns response with all course catalogs in the format:
["results": {"id": 1, "name": "Dummy Catalog"}]
"""
try:
results = get_course_catalogs(site=request.site)
except: # pylint: disable=bare-except
logger.exception('Failed to retrieve course catalogs data from the Course Discovery API.')
results = []
# Create catalogs list with sorting by name
catalogs = [{'id': catalog['id'], 'name': catalog['name']} for catalog in results]
data = {'results': sorted(catalogs, key=lambda catalog: catalog.get('name', '').lower())}
return Response(data=data)
......@@ -122,7 +122,6 @@ class CouponCreationTests(CouponMixin, TestCase):
catalog_query=None,
category=self.category,
code=code,
course_catalog=None,
course_seat_types=None,
email_domains=None,
end_datetime='2020-1-1',
......
......@@ -36,8 +36,7 @@ def create_coupon_product(
quantity,
start_datetime,
title,
voucher_type,
course_catalog,
voucher_type
):
"""
Creates a coupon product and a stock record for it.
......@@ -50,7 +49,6 @@ def create_coupon_product(
category (dict): Contains category ID and name.
code (str): Voucher code.
course_seat_types (str): Comma-separated list of course seat types.
course_catalog (int): Course catalog id from Catalog Service
email_domains (str): Comma-separated list of email domains.
end_datetime (Datetime): Voucher end Datetime.
max_uses (int): Number of Voucher max uses.
......@@ -84,7 +82,6 @@ def create_coupon_product(
catalog_query=catalog_query,
code=code or None,
coupon=coupon_product,
course_catalog=course_catalog,
course_seat_types=course_seat_types,
email_domains=email_domains,
end_datetime=end_datetime,
......
......@@ -2,9 +2,8 @@ from oscar.apps.offer.admin import * # pylint: disable=unused-import,wildcard-i
class RangeAdminExtended(admin.ModelAdmin):
list_display = ('name', 'catalog', 'course_catalog',)
list_display = ('name', 'catalog',)
raw_id_fields = ('catalog',)
search_fields = ['name', 'course_catalog']
admin.site.unregister(Range)
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('offer', '0007_auto_20161026_0856'),
]
operations = [
migrations.AddField(
model_name='range',
name='course_catalog',
field=models.PositiveIntegerField(help_text='Course catalog id from the Catalog Service.', null=True, blank=True),
),
]
......@@ -119,16 +119,10 @@ class Range(AbstractRange):
UPDATABLE_RANGE_FIELDS = [
'catalog_query',
'course_seat_types',
'course_catalog',
]
ALLOWED_SEAT_TYPES = ['credit', 'professional', 'verified']
catalog = models.ForeignKey('catalogue.Catalog', blank=True, null=True, related_name='ranges')
catalog_query = models.TextField(blank=True, null=True)
course_catalog = models.PositiveIntegerField(
help_text=_('Course catalog id from the Catalog Service.'),
null=True,
blank=True
)
course_seat_types = models.CharField(
max_length=255,
validators=[validate_credit_seat_type],
......
......@@ -52,13 +52,5 @@ class Voucher(AbstractVoucher):
logger.exception('Failed to create Voucher. Voucher start and end datetime fields must be type datetime.')
raise ValidationError(_('Voucher start and end datetime fields must be type datetime.'))
@classmethod
def does_exist(cls, code):
try:
Voucher.objects.get(code=code)
return True
except Voucher.DoesNotExist:
return False
from oscar.apps.voucher.models import * # noqa pylint: disable=wildcard-import,unused-wildcard-import,wrong-import-position
......@@ -137,13 +137,6 @@ class UtilTests(CouponMixin, CourseCatalogMockMixin, CourseCatalogTestMixin, Lms
course_seat_types=course_seat_types
)
def create_course_catalog_coupon(self, coupon_title, quantity, course_catalog):
return self.create_coupon(
title=coupon_title,
quantity=quantity,
course_catalog=course_catalog,
)
def use_voucher(self, order_num, voucher, user):
"""
Mark voucher as used by provided users
......@@ -249,39 +242,6 @@ class UtilTests(CouponMixin, CourseCatalogMockMixin, CourseCatalogTestMixin, Lms
with self.assertRaises(IntegrityError):
create_vouchers(**self.data)
def test_create_course_catalog_coupon(self):
"""
Test course catalog coupon voucher creation with specified catalog id.
"""
coupon_title = 'Course catalog coupon'
quantity = 1
course_catalog = 1
course_catalog_coupon = self.create_course_catalog_coupon(
coupon_title=coupon_title,
quantity=quantity,
course_catalog=course_catalog,
)
self.assertEqual(course_catalog_coupon.title, coupon_title)
course_catalog_vouchers = course_catalog_coupon.attr.coupon_vouchers.vouchers.all()
self.assertEqual(course_catalog_vouchers.count(), quantity)
course_catalog_voucher_range = course_catalog_vouchers.first().offers.first().benefit.range
self.assertEqual(course_catalog_voucher_range.course_catalog, course_catalog)
self.data.update({
'name': coupon_title,
'benefit_value': course_catalog_vouchers.first().offers.first().benefit.value,
'code': course_catalog_vouchers.first().code,
'quantity': quantity,
'course_catalog': course_catalog,
'catalog': None,
'course_seat_types': None
})
with self.assertRaises(IntegrityError):
create_vouchers(**self.data)
def assert_report_first_row(self, row, coupon, voucher):
"""
Verify that the first row fields contain the right data.
......
......@@ -418,33 +418,26 @@ def create_vouchers(
_range=None,
catalog_query=None,
course_seat_types=None,
email_domains=None,
course_catalog=None,
):
email_domains=None):
"""
Create vouchers.
Arguments:
benefit_type (str): Type of benefit associated with vouchers.
benefit_value (Decimal): Value of benefit associated with vouchers.
catalog (Catalog): Catalog associated with range of products
to which a voucher can be applied to.
catalog_query (str): ElasticSearch query used by dynamic coupons. Defaults to None.
code (str): Code associated with vouchers. Defaults to None.
coupon (Coupon): Coupon entity associated with vouchers.
course_catalog (int): Course catalog id from Catalog Service. Defaults to None.
course_seat_types (str): Comma-separated list of course seat types.
email_domains (str): List of email domains to restrict coupons. Defaults to None.
end_datetime (datetime): End date for voucher offer.
max_uses (int): Number of Voucher max uses. Defaults to None.
name (str): Voucher name.
quantity (int): Number of vouchers to be created.
start_datetime (datetime): Start date for voucher offer.
voucher_type (str): Type of voucher.
_range (Range): Product range. Defaults to None.
Args:
benefit_type (str): Type of benefit associated with vouchers.
benefit_value (Decimal): Value of benefit associated with vouchers.
catalog (Catalog): Catalog associated with range of products
to which a voucher can be applied to.
coupon (Coupon): Coupon entity associated with vouchers.
end_datetime (datetime): End date for voucher offer.
name (str): Voucher name.
quantity (int): Number of vouchers to be created.
start_datetime (datetime): Start date for voucher offer.
voucher_type (str): Type of voucher.
code (str): Code associated with vouchers. Defaults to None.
email_domains (str): List of email domains to restrict coupons. Defaults to None.
Returns:
List[Voucher]
List[Voucher]
"""
logger.info("Creating [%d] vouchers product [%s]", quantity, coupon.id)
vouchers = []
......@@ -457,13 +450,10 @@ def create_vouchers(
else:
logger.info("Creating [%d] vouchers for coupon [%s]", quantity, coupon.id)
range_name = (_('Range for coupon [{coupon_id}]').format(coupon_id=coupon.id))
# make sure course catalog is None if its empty
course_catalog = course_catalog if course_catalog else None
product_range, __ = Range.objects.get_or_create(
name=range_name,
catalog=catalog,
catalog_query=catalog_query,
course_catalog=course_catalog,
course_seat_types=course_seat_types,
)
......
require([
'backbone',
'collections/category_collection',
'collections/catalog_collection',
'ecommerce',
'routers/coupon_router',
'utils/navigate',
],
function (Backbone,
CategoryCollection,
CatalogCollection,
ecommerce,
CouponRouter,
navigate) {
......@@ -22,10 +20,6 @@ require([
ecommerce.coupons.categories = new CategoryCollection();
ecommerce.coupons.categories.url = '/api/v2/coupons/categories/';
ecommerce.coupons.categories.fetch({ async: false });
ecommerce.coupons.catalogs = new CatalogCollection();
ecommerce.coupons.catalogs.fetch({ async: false });
couponApp.start();
// Handle navbar clicks.
......
define([
'collections/paginated_collection',
'models/catalog_model'
],
function (PaginatedCollection,
Catalog) {
'use strict';
return PaginatedCollection.extend({
model: Catalog,
url: '/api/v2/catalogs/course_catalogs/'
}
);
}
);
define([
'backbone.relational'
],
function () {
'use strict';
return Backbone.RelationalModel.extend({
urlRoot: '/api/v2/catalogs/course_catalogs/'
});
}
);
......@@ -7,9 +7,7 @@ define([
'underscore',
'moment',
'collections/category_collection',
'collections/catalog_collection',
'models/category',
'models/catalog_model',
'utils/validation_patterns'
],
function (Backbone,
......@@ -26,23 +24,15 @@ define([
required: gettext('This field is required.'),
number: gettext('This value must be a number.'),
date: gettext('This value must be a date.'),
seat_types: gettext('At least one seat type must be selected.')
seat_types: gettext('At least one seat type must be selected.'),
});
_.extend(Backbone.Model.prototype, Backbone.Validation.mixin);
/* jshint esnext: true */
var CATALOG_TYPES = {
single_course: 'Single course',
multiple_courses: 'Multiple courses',
catalog: 'Catalog'
};
return Backbone.RelationalModel.extend({
urlRoot: '/api/v2/coupons/',
defaults: {
category: {id: 3, name: 'Affiliate Promotion'},
course_catalog: {id: '', name: ''},
code: '',
course_seats: [],
course_seat_types: [],
......@@ -52,11 +42,9 @@ define([
quantity: 1,
seats: [],
stock_record_ids: [],
total_value: 0
total_value: 0,
},
catalogTypes: CATALOG_TYPES,
validation: {
benefit_value: {
pattern: 'number',
......@@ -66,12 +54,7 @@ define([
},
catalog_query: {
required: function () {
return this.get('catalog_type') === CATALOG_TYPES.multiple_courses;
}
},
course_catalog: {
required: function () {
return this.get('catalog_type') === CATALOG_TYPES.catalog;
return this.get('catalog_type') === 'Multiple courses';
}
},
category: {required: true},
......@@ -86,11 +69,11 @@ define([
pattern: 'courseId',
msg: gettext('A valid course ID is required'),
required: function () {
return this.get('catalog_type') === CATALOG_TYPES.single_course;
return this.get('catalog_type') === 'Single course';
}
},
course_seat_types: function (val) {
if (this.get('catalog_type') === CATALOG_TYPES.multiple_courses && val.length === 0) {
if (this.get('catalog_type') === 'Multiple courses' && val.length === 0) {
return Backbone.Validation.messages.seat_types;
}
},
......@@ -150,7 +133,7 @@ define([
// seat_type is for validation only, stock_record_ids holds the values
seat_type: {
required: function () {
return this.get('catalog_type') === CATALOG_TYPES.single_course;
return this.get('catalog_type') === 'Single course';
}
},
start_date: function (val) {
......@@ -168,7 +151,7 @@ define([
return gettext('Must occur before end date');
}
},
title: {required: true}
title: {required: true},
},
initialize: function () {
......@@ -202,29 +185,11 @@ define([
updateSeatData: function () {
var seat_data,
seats = this.get('seats'),
catalogId,
catalogType;
seats = this.get('seats');
catalogId = '';
if (this.has('course_catalog')) {
if (typeof this.get('course_catalog') === 'number') {
catalogId = this.get('course_catalog');
} else if (!$.isEmptyObject(this.get('course_catalog'))) {
catalogId = this.get('course_catalog').id;
}
}
if (this.has('catalog_query') && this.get('catalog_query') !== '') {
catalogType = this.catalogTypes.multiple_courses;
} else if (catalogId !== '') {
catalogType = this.catalogTypes.catalog;
} else {
catalogType = this.catalogTypes.single_course;
}
this.set('catalog_type', catalogType);
this.set('catalog_type', this.has('catalog_query') ? 'Multiple courses': 'Single course');
if (this.get('catalog_type') === this.catalogTypes.single_course) {
if (this.get('catalog_type') === 'Single course') {
if (seats[0]) {
seat_data = seats[0].attribute_values;
......@@ -232,7 +197,6 @@ define([
this.set('course_id', this.getCourseID(seat_data));
this.updateTotalValue(this.getSeatPrice());
}
this.set('course_catalog', this.defaults.course_catalog);
}
},
......@@ -247,7 +211,7 @@ define([
'invoice_number': invoice.number,
'invoice_payment_date': invoice.payment_date,
'tax_deducted_source': invoice.tax_deducted_source,
'tax_deduction': tax_deducted
'tax_deduction': tax_deducted,
});
},
......
define([], function(){
'use strict';
var catalogs = [
{
'id': 1,
'name': 'All Courses Catalog'
},
{
'id': 2,
'name': 'No Courses Catalog'
},
{
'id': 3,
'name': 'edX Catalog'
},
{
'id': 4,
'name': 'Test Catalog 1'
},
{
'id': 5,
'name': 'Enterprise Catalog'
},
{
'id': 6,
'name': 'Test 2 Catalog'
},
{
'id': 7,
'name': 'Empty Catalog'
}
];
return catalogs;
});
......@@ -194,7 +194,6 @@ define([], function () {
'id': 4,
'name': 'TESTCAT'
},
course_catalog: {},
'price': '100.00',
'invoice_type': 'Prepaid',
'invoice_discount_type': 'Percentage',
......
define([], function(){
'use strict';
var selected_catalog = {
1 : {
'id': 1,
'name': 'Courses Catalog 1'
},
2 : {
'id': 2,
'name': 'Courses Catalog 2'
}
};
return selected_catalog;
});
......@@ -20,8 +20,6 @@ if (!window.gettext) {
// Establish the global namespace
window.ecommerce = window.ecommerce || {};
window.ecommerce.coupons = window.ecommerce.coupons || {};
window.ecommerce.catalogs = window.ecommerce.catalogs || {};
window.ecommerce.categories = window.ecommerce.categories || {};
window.ecommerce.credit = window.ecommerce.credit || {};
// you can automatically get the test files using karma's configs
......
......@@ -14,13 +14,7 @@ define([
return Backbone.Model.extend({
defaults: {
id: null,
category: {},
course_catalog: {}
},
catalogTypes: {
single_course: 'Single course',
multiple_courses: 'Multiple courses',
catalog: 'Catalog'
category: {}
},
isValid: function () {
......
define([
'collections/catalog_collection',
'test/mock_data/catalogs'
],
function (CatalogCollection,
Mock_Catalogs) {
'use strict';
var collection,
response = Mock_Catalogs;
beforeEach(function () {
collection = new CatalogCollection();
});
describe('Catalog collection', function () {
describe('parse', function () {
it('should fetch the next page of results', function () {
spyOn(collection, 'fetch').and.returnValue(null);
response.next = '/api/v2/catalogs/course_catalogs/?page=2';
collection.parse(response);
expect(collection.url).toEqual(response.next);
expect(collection.fetch).toHaveBeenCalledWith({remove: false});
});
});
});
}
);
......@@ -81,32 +81,12 @@ define([
model.validate();
expect(model.isValid()).toBeFalsy();
model.set('catalog_query', '');
model.set('course_seat_types', ['verified']);
model.validate();
expect(model.isValid()).toBe(false);
model.set('catalog_query', '*:*');
model.set('course_seat_types', ['verified']);
model.validate();
expect(model.isValid()).toBeTruthy();
});
it('should validate course catalog for type Catalog', function () {
model.set('catalog_type', 'Catalog');
model.set('course_catalog', '');
model.validate();
expect(model.isValid()).toBe(false);
model.set('course_catalog', '');
model.validate();
expect(model.isValid()).toBe(false);
model.set('course_catalog', '1');
model.validate();
expect(model.isValid()).toBe(true);
});
it('should validate invoice data.', function() {
model.set('price', 'text');
model.validate();
......@@ -193,36 +173,6 @@ define([
});
});
describe('Should Update Seat Data Correctly.', function () {
it('should set correct catalog type for each seat.', function () {
// Test single course catalog type when a single course is selected for coupon creation.
var model = new Coupon({});
model.updateSeatData();
expect(model.get('catalog_type')).toEqual(model.catalogTypes.single_course);
// Test course catalog type when an existing course catalog is selected for coupon creation.
model = new Coupon({
course_catalog: {id: 1, name: 'Test Catalog'}
});
model.updateSeatData();
expect(model.get('catalog_type')).toEqual(model.catalogTypes.catalog);
model = new Coupon({
course_catalog: 1
});
model.updateSeatData();
expect(model.get('catalog_type')).toEqual(model.catalogTypes.catalog);
// Test multiple course type when an catalog query is given for coupon creation.
model = new Coupon({
catalog_query: '*:*'
});
model.updateSeatData();
expect(model.get('catalog_type')).toEqual(model.catalogTypes.multiple_courses);
});
});
describe('save', function () {
it('should POST enrollment data', function () {
var model, args, ajaxData;
......
......@@ -4,7 +4,6 @@ define([
'utils/utils',
'views/coupon_form_view',
'test/mock_data/categories',
'test/mock_data/catalogs',
'ecommerce'
],
function (Backbone,
......@@ -12,7 +11,6 @@ define([
Utils,
CouponFormView,
Mock_Categories,
Mock_Catalogs,
ecommerce) {
'use strict';
......@@ -67,8 +65,7 @@ define([
beforeEach(function () {
ecommerce.coupons = {
categories: Mock_Categories,
catalogs: Mock_Catalogs
categories: Mock_Categories
};
});
......
......@@ -4,7 +4,6 @@ define([
'views/alert_view',
'models/coupon_model',
'test/mock_data/categories',
'test/mock_data/catalogs',
'ecommerce'
],
function ($,
......@@ -12,7 +11,6 @@ define([
AlertView,
Coupon,
Mock_Categories,
Mock_Catalogs,
ecommerce) {
'use strict';
......@@ -22,8 +20,7 @@ define([
beforeEach(function () {
ecommerce.coupons = {
categories: Mock_Categories,
catalogs: Mock_Catalogs
categories: Mock_Categories
};
model = new Coupon();
view = new CouponCreateEditView({ model: model, editing: false }).render();
......
......@@ -6,8 +6,6 @@ define([
'models/coupon_model',
'test/mock_data/categories',
'test/mock_data/coupons',
'test/mock_data/catalogs',
'test/mock_data/selected_catalogs',
'test/spec-utils',
'ecommerce',
'test/custom-matchers'
......@@ -19,8 +17,6 @@ define([
Coupon,
Mock_Categories,
Mock_Coupons,
Mock_Catalogs,
Mock_Selected_Catalogs,
SpecUtils,
ecommerce) {
'use strict';
......@@ -32,10 +28,9 @@ define([
beforeEach(function () {
ecommerce.coupons = {
categories: Mock_Categories,
catalogs: Mock_Catalogs
categories: Mock_Categories
};
model = new Coupon({course_catalog: Mock_Catalogs});
model = new Coupon();
view = new CouponFormView({ editing: false, model: model }).render();
});
......@@ -129,50 +124,6 @@ define([
});
});
describe('course catalogs', function() {
it('course catalog drop down should be hidden when catalog is not selected', function() {
view.$('#single-course').prop('checked', true).trigger('change');
expect(SpecUtils.formGroup(view, '[name=course_catalog]')).not.toBeVisible();
view.$('#multiple-courses').prop('checked', true).trigger('change');
expect(SpecUtils.formGroup(view, '[name=course_catalog]')).not.toBeVisible();
view.$('#catalog').prop('checked', true).trigger('change');
expect(SpecUtils.formGroup(view, '[name=course_catalog]')).toBeVisible();
});
it('course catalog is setting properly', function() {
view.$('#catalog').prop('checked', true).trigger('change');
view.$('[name=course_catalog]').val(1).trigger('change');
expect(view.$('select[name=course_catalog] option:selected').text()).toEqual(Mock_Catalogs[0].name);
expect(view.$('[name=course_catalog]').val()).toEqual('1');
view.$('[name=course_catalog]').val(2).trigger('change');
expect(view.$('select[name=course_catalog] option:selected').text()).toEqual(Mock_Catalogs[1].name);
expect(view.$('[name=course_catalog]').val()).toEqual('2');
view.$('[name=course_catalog]').val(3).trigger('change');
expect(view.$('select[name=course_catalog] option:selected').text()).toEqual(Mock_Catalogs[2].name);
expect(view.$('[name=course_catalog]').val()).toEqual('3');
});
it('returning right course catalog when selected catalog is number', function() {
ecommerce.coupons = {
categories: Mock_Categories,
catalogs: Mock_Selected_Catalogs
};
var coupon_model = new Coupon({course_catalog: 1});
var coupon_form_view = new CouponFormView({ editing: true, model: coupon_model }).render();
expect(coupon_model.get('course_catalog')).toEqual({id: 1,'name': 'Courses Catalog 1'});
coupon_model.set({course_catalog: 2});
coupon_form_view.render();
expect(coupon_model.get('course_catalog')).toEqual({id: 2,'name': 'Courses Catalog 2'});
});
});
describe('discount code', function () {
var prepaid_invoice_fields = [
'[name=invoice_number]',
......
define([
'jquery',
'backbone',
'ecommerce',
'underscore',
'underscore.string',
'moment',
......@@ -12,7 +11,6 @@ define([
],
function ($,
Backbone,
ecommerce,
_,
_s,
moment,
......@@ -116,8 +114,6 @@ define([
render: function () {
var html,
category = this.model.get('category').name,
courseCatalog = this.model.get('course_catalog'),
courseCatalogName = '',
invoice_data = this.formatInvoiceData(),
emailDomains = this.model.get('email_domains'),
template_data,
......@@ -127,16 +123,9 @@ define([
price = _s.sprintf('$%s', this.model.get('price'));
}
if (typeof courseCatalog === 'number') {
courseCatalogName = ecommerce.coupons.catalogs.get(courseCatalog).get('name');
} else if (!$.isEmptyObject(courseCatalog)) {
courseCatalogName = courseCatalog.name;
}
template_data = {
category: category,
coupon: this.model.toJSON(),
courseCatalogName: courseCatalogName,
courseSeatType: this.formatSeatTypes(),
discountValue: this.discountValue(),
endDateTime: this.formatDateTime(this.model.get('end_date')),
......
......@@ -168,28 +168,6 @@ define([
'input[name=course_seat_types]': {
observe: 'course_seat_types'
},
'select[name=course_catalog]': {
observe: 'course_catalog',
selectOptions: {
collection: function () {
return ecommerce.coupons.catalogs;
},
labelPath: 'name',
valuePath: 'id'
},
setOptions: {
validate: true
},
onGet: function (val) {
return val.id;
},
onSet: function (val) {
return {
id: val,
name: $('select[name=course_catalog] option:selected').text()
};
}
},
'input[name=email_domains]': {
observe: 'email_domains',
onSet: function(val) {
......@@ -274,7 +252,6 @@ define([
'category',
'client',
'course_seat_types',
'course_catalog',
'end_date',
'invoice_discount_type',
'invoice_discount_value',
......@@ -440,37 +417,22 @@ define([
},
toggleCatalogTypeField: function() {
if (this.model.get('catalog_type') === this.model.catalogTypes.single_course) {
if (this.model.get('catalog_type') === 'Single course') {
this.model.unset('course_seat_types');
this.model.unset('catalog_query');
this.model.set('course_catalog', this.model.defaults.course_catalog);
this.formGroup('[name=catalog_query]').addClass(this.hiddenClass);
this.formGroup('[name=course_seat_types]').addClass(this.hiddenClass);
this.formGroup('[name=course_id]').removeClass(this.hiddenClass);
this.formGroup('[name=seat_type]').removeClass(this.hiddenClass);
this.formGroup('[name=course_catalog]').addClass(this.hiddenClass);
} else if (this.model.get('catalog_type') === this.model.catalogTypes.catalog) {
this.model.unset('course_id');
this.model.unset('seat_type');
this.model.unset('stock_record_ids');
this.model.unset('catalog_query');
this.model.unset('course_seat_types');
this.formGroup('[name=catalog_query]').addClass(this.hiddenClass);
this.formGroup('[name=course_seat_types]').addClass(this.hiddenClass);
this.formGroup('[name=course_id]').addClass(this.hiddenClass);
this.formGroup('[name=seat_type]').addClass(this.hiddenClass);
this.formGroup('[name=course_catalog]').removeClass(this.hiddenClass);
} else {
this.formGroup('[name=catalog_query]').removeClass(this.hiddenClass);
this.formGroup('[name=course_seat_types]').removeClass(this.hiddenClass);
this.formGroup('[name=course_id]').addClass(this.hiddenClass);
this.formGroup('[name=seat_type]').addClass(this.hiddenClass);
this.formGroup('[name=course_catalog]').addClass(this.hiddenClass);
this.$('[name=seat_type] option').remove();
this.model.unset('course_id');
this.model.unset('seat_type');
this.model.unset('stock_record_ids');
this.model.set('course_catalog', this.model.defaults.course_catalog);
if (!this.model.get('course_seat_types')) {
this.model.set('course_seat_types', []);
......@@ -652,8 +614,6 @@ define([
render: function () {
// Render the parent form/template
var catalogId = '';
this.$el.html(this.template(this.model.attributes));
this.stickit();
......@@ -670,21 +630,6 @@ define([
this.$('.non-credit-seats').addClass(this.hiddenClass);
}
}
if (this.model.has('course_catalog')) {
if (typeof this.model.get('course_catalog') === 'number') {
catalogId = this.model.get('course_catalog');
} else if (!$.isEmptyObject(this.model.get('course_catalog'))) {
catalogId = this.model.get('course_catalog').id;
}
}
if (catalogId !== '') {
this.model.set('course_catalog', ecommerce.coupons.catalogs[catalogId]);
} else {
this.model.set('course_catalog', this.model.defaults.course_catalog);
}
this.disableNonEditableFields();
this.toggleCouponTypeField();
this.toggleVoucherTypeField();
......@@ -700,7 +645,7 @@ define([
'coupon_type': this.codeTypes[0].value,
'voucher_type': this.voucherTypes[0].value,
'benefit_type': 'Percentage',
'catalog_type': this.model.catalogTypes.single_course,
'catalog_type': 'Single course',
'invoice_discount_type': 'Percentage',
'invoice_type': 'Prepaid',
'tax_deduction': 'No',
......
......@@ -50,12 +50,6 @@
<div class="value"><%= coupon.catalog_query %></div>
</div>
<%}%>
<% if(courseCatalogName) {%>
<div class="info-item grid-item catalog-name">
<div class="heading"><%= gettext('Catalog:') %></div>
<div class="value"><%= courseCatalogName %></div>
</div>
<%}%>
<div class="info-item grid-item date-info">
<div class="start-date-info">
<div class="heading"><%= gettext('Valid from:') %></div>
......
......@@ -164,8 +164,6 @@
<label for="single-course"><%= gettext('Single course') %></label>
<input id="multiple-courses" type="radio" name="catalog_type" value="Multiple courses">
<label for="multiple-courses"><%= gettext('Multiple courses') %></label>
<input id="catalog" type="radio" name="catalog_type" value="Catalog">
<label for="catalog"><%= gettext('Catalog') %></label>
</div>
<p class="help-block"></p>
</div>
......@@ -206,11 +204,6 @@
</div>
<div class="catalog_buttons"></div>
</div>
<div class="form-group course-catalog">
<label for="course-catalog"><%= gettext('Select from course catalogs:') %> *</label>
<select id="course-catalog" class="form-control" name="course_catalog"></select>
<p class="help-block"></p>
</div>
<div class="form-group email-domains">
<label for="email-domains"><%= gettext('Email domains:') %> </label>
<input id="email-domains" class="form-control" name="email_domains" maxlength="255">
......
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