Commit 28ffaa5a by Renzo Lucioni

Merge pull request #251 from edx/renzo/audit-migration

Support migration of legacy audit modes
parents 02be1db8 473e615d
...@@ -72,13 +72,16 @@ class Course(models.Model): ...@@ -72,13 +72,16 @@ class Course(models.Model):
if mode == 'no-id-professional': if mode == 'no-id-professional':
return 'professional' return 'professional'
elif mode == 'audit':
# Historically, users enrolled in an 'audit' mode have not received a certificate.
return ''
return mode return mode
@property @property
def type(self): def type(self):
""" Returns the type of the course (based on the available seat types). """ """ Returns the type of the course (based on the available seat types). """
seat_types = [(seat.attr.certificate_type or '').lower() for seat in self.seat_products] seat_types = [getattr(seat.attr, 'certificate_type', '').lower() for seat in self.seat_products]
if 'credit' in seat_types: if 'credit' in seat_types:
return 'credit' return 'credit'
elif 'professional' in seat_types or 'no-id-professional' in seat_types: elif 'professional' in seat_types or 'no-id-professional' in seat_types:
...@@ -100,12 +103,13 @@ class Course(models.Model): ...@@ -100,12 +103,13 @@ class Course(models.Model):
def _get_course_seat_name(self, certificate_type, id_verification_required): def _get_course_seat_name(self, certificate_type, id_verification_required):
""" Returns the name for a course seat. """ """ Returns the name for a course seat. """
name = 'Seat in {course_name} with {certificate_type} certificate'.format( name = u'Seat in {}'.format(self.name)
course_name=self.name,
certificate_type=certificate_type)
if id_verification_required: if certificate_type != '':
name += ' (and ID verification)' name += u' with {} certificate'.format(certificate_type)
if id_verification_required:
name += u' (and ID verification)'
return name return name
...@@ -117,7 +121,6 @@ class Course(models.Model): ...@@ -117,7 +121,6 @@ class Course(models.Model):
Returns: Returns:
Product: The seat that has been created or updated. Product: The seat that has been created or updated.
""" """
certificate_type = certificate_type.lower() certificate_type = certificate_type.lower()
course_id = unicode(self.id) course_id = unicode(self.id)
...@@ -136,12 +139,18 @@ class Course(models.Model): ...@@ -136,12 +139,18 @@ class Course(models.Model):
try: try:
seat = Product.objects.get(slug__in=slugs) seat = Product.objects.get(slug__in=slugs)
logger.info('Retrieved [%s] course seat child product for [%s] from database.', certificate_type, logger.info(
course_id) 'Retrieved course seat child product with certificate type [%s] for [%s] from database.',
certificate_type,
course_id
)
except Product.DoesNotExist: except Product.DoesNotExist:
seat = Product(slug=slug) seat = Product(slug=slug)
logger.info('[%s] course seat product for [%s] does not exist. Instantiated a new instance.', logger.info(
certificate_type, course_id) 'Course seat product with certificate type [%s] for [%s] does not exist. Instantiated a new instance.',
certificate_type,
course_id
)
seat.course = self seat.course = self
seat.parent = self.parent_seat_product seat.parent = self.parent_seat_product
...@@ -149,7 +158,11 @@ class Course(models.Model): ...@@ -149,7 +158,11 @@ class Course(models.Model):
seat.structure = Product.CHILD seat.structure = Product.CHILD
seat.title = self._get_course_seat_name(certificate_type, id_verification_required) seat.title = self._get_course_seat_name(certificate_type, id_verification_required)
seat.expires = expires seat.expires = expires
# If a ProductAttribute is saved with a value of None or the empty string, the ProductAttribute is deleted.
# As a consequence, Seats derived from a migrated "audit" mode do not have a certificate_type attribute.
seat.attr.certificate_type = certificate_type seat.attr.certificate_type = certificate_type
seat.attr.course_key = course_id seat.attr.course_key = course_id
seat.attr.id_verification_required = id_verification_required seat.attr.id_verification_required = id_verification_required
...@@ -165,14 +178,20 @@ class Course(models.Model): ...@@ -165,14 +178,20 @@ class Course(models.Model):
partner = Partner.objects.get(code='edx') partner = Partner.objects.get(code='edx')
try: try:
stock_record = StockRecord.objects.get(product=seat, partner=partner) stock_record = StockRecord.objects.get(product=seat, partner=partner)
logger.info('Retrieved [%s] course seat child product stock record for [%s] from database.', logger.info(
certificate_type, course_id) 'Retrieved course seat product stock record with certificate type [%s] for [%s] from database.',
certificate_type,
course_id
)
except StockRecord.DoesNotExist: except StockRecord.DoesNotExist:
partner_sku = generate_sku(seat) partner_sku = generate_sku(seat)
stock_record = StockRecord(product=seat, partner=partner, partner_sku=partner_sku) stock_record = StockRecord(product=seat, partner=partner, partner_sku=partner_sku)
logger.info( logger.info(
'[%s] course seat product stock record for [%s] does not exist. Instantiated a new instance.', 'Course seat product stock record with certificate type [%s] for [%s] does not exist. '
certificate_type, course_id) 'Instantiated a new instance.',
certificate_type,
course_id
)
stock_record.price_excl_tax = price stock_record.price_excl_tax = price
......
...@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) ...@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
class LMSPublisher(object): class LMSPublisher(object):
def get_seat_expiration(self, seat): def get_seat_expiration(self, seat):
if not seat.expires or 'professional' in seat.attr.certificate_type: if not seat.expires or 'professional' in getattr(seat.attr, 'certificate_type', ''):
return None return None
return seat.expires.isoformat() return seat.expires.isoformat()
......
...@@ -59,7 +59,7 @@ class CourseTests(CourseCatalogTestMixin, TestCase): ...@@ -59,7 +59,7 @@ class CourseTests(CourseCatalogTestMixin, TestCase):
('professional', 'professional'), ('professional', 'professional'),
('honor', 'honor'), ('honor', 'honor'),
('no-id-professional', 'professional'), ('no-id-professional', 'professional'),
('audit', 'audit'), ('audit', ''),
('unknown', 'unknown'), ('unknown', 'unknown'),
) )
@ddt.unpack @ddt.unpack
...@@ -92,7 +92,7 @@ class CourseTests(CourseCatalogTestMixin, TestCase): ...@@ -92,7 +92,7 @@ class CourseTests(CourseCatalogTestMixin, TestCase):
# pylint: disable=protected-access # pylint: disable=protected-access
self.assertEqual(seat.title, course._get_course_seat_name(certificate_type, id_verification_required)) self.assertEqual(seat.title, course._get_course_seat_name(certificate_type, id_verification_required))
self.assertEqual(seat.get_product_class(), self.seat_product_class) self.assertEqual(seat.get_product_class(), self.seat_product_class)
self.assertEqual(seat.attr.certificate_type, certificate_type) self.assertEqual(getattr(seat.attr, 'certificate_type', ''), certificate_type)
self.assertEqual(seat.attr.course_key, course.id) self.assertEqual(seat.attr.course_key, course.id)
self.assertEqual(seat.attr.id_verification_required, id_verification_required) self.assertEqual(seat.attr.id_verification_required, id_verification_required)
self.assertEqual(seat.stockrecords.first().price_excl_tax, price) self.assertEqual(seat.stockrecords.first().price_excl_tax, price)
......
...@@ -98,7 +98,7 @@ class LMSPublisherTests(CourseCatalogTestMixin, TestCase): ...@@ -98,7 +98,7 @@ class LMSPublisherTests(CourseCatalogTestMixin, TestCase):
def test_serialize_seat_for_commerce_api(self): def test_serialize_seat_for_commerce_api(self):
""" The method should convert a seat to a JSON-serializable dict consumable by the Commerce API. """ """ The method should convert a seat to a JSON-serializable dict consumable by the Commerce API. """
# Grab the verified seat # Grab the verified seat
seat = sorted(self.course.seat_products, key=lambda p: p.attr.certificate_type)[1] seat = sorted(self.course.seat_products, key=lambda p: getattr(p.attr, 'certificate_type', ''))[1]
stock_record = seat.stockrecords.first() stock_record = seat.stockrecords.first()
actual = self.publisher.serialize_seat_for_commerce_api(seat) actual = self.publisher.serialize_seat_for_commerce_api(seat)
......
...@@ -11,6 +11,7 @@ from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin ...@@ -11,6 +11,7 @@ from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin
class UtilsTests(CourseCatalogTestMixin, TestCase): class UtilsTests(CourseCatalogTestMixin, TestCase):
@ddt.unpack @ddt.unpack
@ddt.data( @ddt.data(
('', False, 'audit'),
('honor', True, 'honor'), ('honor', True, 'honor'),
('honor', False, 'honor'), ('honor', False, 'honor'),
('verified', True, 'verified'), ('verified', True, 'verified'),
......
def mode_for_seat(seat): def mode_for_seat(seat):
""" Returns the Enrollment mode for a given seat product. """ """ Returns the Enrollment mode for a given seat product. """
certificate_type = seat.attr.certificate_type certificate_type = getattr(seat.attr, 'certificate_type', '')
if certificate_type == 'professional' and not seat.attr.id_verification_required: if certificate_type == 'professional' and not seat.attr.id_verification_required:
return 'no-id-professional' return 'no-id-professional'
elif certificate_type == '':
return 'audit'
return certificate_type return certificate_type
...@@ -39,7 +39,7 @@ class Checkout(TemplateView): ...@@ -39,7 +39,7 @@ class Checkout(TemplateView):
processors_dict[processor] = 'Checkout with {}'.format(processor) processors_dict[processor] = 'Checkout with {}'.format(processor)
credit_seats = [ credit_seats = [
seat for seat in course.seat_products if seat.attr.certificate_type == self.CREDIT_MODE seat for seat in course.seat_products if getattr(seat.attr, 'certificate_type', '') == self.CREDIT_MODE
] ]
provider_ids = None provider_ids = None
if credit_seats: if credit_seats:
......
...@@ -138,9 +138,14 @@ class Command(BaseCommand): ...@@ -138,9 +138,14 @@ class Command(BaseCommand):
for seat in course.seat_products: for seat in course.seat_products:
stock_record = seat.stockrecords.first() stock_record = seat.stockrecords.first()
data = (seat.attr.certificate_type, seat.attr.id_verification_required, data = (
'{0} {1}'.format(stock_record.price_currency, stock_record.price_excl_tax), getattr(seat.attr, 'certificate_type', ''),
stock_record.partner_sku, seat.slug, seat.expires) seat.attr.id_verification_required,
'{0} {1}'.format(stock_record.price_currency, stock_record.price_excl_tax),
stock_record.partner_sku,
seat.slug,
seat.expires
)
msg += '\t{}\n'.format(data) msg += '\t{}\n'.format(data)
logger.info(msg) logger.info(msg)
......
...@@ -19,6 +19,7 @@ from waffle import Switch ...@@ -19,6 +19,7 @@ from waffle import Switch
from ecommerce.core.constants import ISO_8601_FORMAT from ecommerce.core.constants import ISO_8601_FORMAT
from ecommerce.courses.models import Course from ecommerce.courses.models import Course
from ecommerce.courses.publishers import LMSPublisher from ecommerce.courses.publishers import LMSPublisher
from ecommerce.courses.utils import mode_for_seat
from ecommerce.extensions.catalogue.management.commands.migrate_course import MigratedCourse from ecommerce.extensions.catalogue.management.commands.migrate_course import MigratedCourse
from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin
from ecommerce.extensions.catalogue.utils import generate_sku from ecommerce.extensions.catalogue.utils import generate_sku
...@@ -44,7 +45,9 @@ class CourseMigrationTestMixin(CourseCatalogTestMixin): ...@@ -44,7 +45,9 @@ class CourseMigrationTestMixin(CourseCatalogTestMixin):
'honor': 0, 'honor': 0,
'verified': 10, 'verified': 10,
'no-id-professional': 100, 'no-id-professional': 100,
'professional': 1000 'professional': 1000,
'audit': 0,
'credit': 0,
} }
def _mock_lms_api(self): def _mock_lms_api(self):
...@@ -60,8 +63,8 @@ class CourseMigrationTestMixin(CourseCatalogTestMixin): ...@@ -60,8 +63,8 @@ class CourseMigrationTestMixin(CourseCatalogTestMixin):
# Mock Enrollment API # Mock Enrollment API
body = { body = {
'course_id': self.course_id, 'course_id': self.course_id,
'course_modes': [{'slug': seat_type, 'min_price': price, 'expiration_datetime': EXPIRES_STRING} for 'course_modes': [{'slug': mode, 'min_price': price, 'expiration_datetime': EXPIRES_STRING} for
seat_type, price in self.prices.iteritems()] mode, price in self.prices.iteritems()]
} }
httpretty.register_uri(httpretty.GET, self.enrollment_api_url, body=json.dumps(body), content_type=JSON) httpretty.register_uri(httpretty.GET, self.enrollment_api_url, body=json.dumps(body), content_type=JSON)
...@@ -76,12 +79,15 @@ class CourseMigrationTestMixin(CourseCatalogTestMixin): ...@@ -76,12 +79,15 @@ class CourseMigrationTestMixin(CourseCatalogTestMixin):
""" Verify the given seat is configured correctly. """ """ Verify the given seat is configured correctly. """
certificate_type = Course.certificate_type_for_mode(mode) certificate_type = Course.certificate_type_for_mode(mode)
expected_title = 'Seat in A Tést Côurse with {} certificate'.format(certificate_type) expected_title = 'Seat in A Tést Côurse'
if seat.attr.id_verification_required: if certificate_type != '':
expected_title += u' (and ID verification)' expected_title += ' with {} certificate'.format(certificate_type)
if seat.attr.id_verification_required:
expected_title += u' (and ID verification)'
self.assertEqual(seat.title, expected_title) self.assertEqual(seat.title, expected_title)
self.assertEqual(seat.attr.certificate_type, certificate_type) self.assertEqual(getattr(seat.attr, 'certificate_type', ''), certificate_type)
self.assertEqual(seat.expires, EXPIRES) self.assertEqual(seat.expires, EXPIRES)
self.assertEqual(seat.attr.course_key, self.course_id) self.assertEqual(seat.attr.course_key, self.course_id)
self.assertEqual(seat.attr.id_verification_required, Course.is_mode_verified(mode)) self.assertEqual(seat.attr.id_verification_required, Course.is_mode_verified(mode))
...@@ -90,18 +96,20 @@ class CourseMigrationTestMixin(CourseCatalogTestMixin): ...@@ -90,18 +96,20 @@ class CourseMigrationTestMixin(CourseCatalogTestMixin):
""" Verify the course was migrated and saved to the database. """ """ Verify the course was migrated and saved to the database. """
course = Course.objects.get(id=self.course_id) course = Course.objects.get(id=self.course_id)
seats = course.seat_products seats = course.seat_products
self.assertEqual(len(seats), 4)
# Verify that all modes are migrated.
self.assertEqual(len(seats), len(self.prices))
parent = course.products.get(structure=Product.PARENT) parent = course.products.get(structure=Product.PARENT)
self.assertEqual(list(parent.categories.all()), [self.category]) self.assertEqual(list(parent.categories.all()), [self.category])
for seat in seats: for seat in seats:
seat_type = seat.attr.certificate_type mode = mode_for_seat(seat)
if seat_type == 'professional' and not seat.attr.id_verification_required: logger.info('Validating objects for [%s] mode...', mode)
seat_type = 'no-id-professional'
logger.info('Validating objects for %s certificate type...', seat_type)
stock_record = self.partner.stockrecords.get(product=seat) stock_record = self.partner.stockrecords.get(product=seat)
self.assert_seat_valid(seat, seat_type) self.assert_seat_valid(seat, mode)
self.assert_stock_record_valid(stock_record, seat, self.prices[seat_type]) self.assert_stock_record_valid(stock_record, seat, self.prices[mode])
def assert_lms_api_headers(self, request): def assert_lms_api_headers(self, request):
self.assertEqual(request.headers['Accept'], JSON) self.assertEqual(request.headers['Accept'], JSON)
...@@ -143,11 +151,10 @@ class MigratedCourseTests(CourseMigrationTestMixin, TestCase): ...@@ -143,11 +151,10 @@ class MigratedCourseTests(CourseMigrationTestMixin, TestCase):
self.assertEqual(course.verification_deadline, EXPIRES) self.assertEqual(course.verification_deadline, EXPIRES)
for seat in course.seat_products: for seat in course.seat_products:
certificate_type = seat.attr.certificate_type mode = mode_for_seat(seat)
if certificate_type == 'professional' and not seat.attr.id_verification_required: logger.info('Validating objects for [%s] mode...', mode)
certificate_type = 'no-id-professional'
logger.info('Validating objects for %s certificate type...', certificate_type) self.assert_stock_record_valid(seat.stockrecords.first(), seat, Decimal(self.prices[mode]))
self.assert_stock_record_valid(seat.stockrecords.first(), seat, Decimal(self.prices[certificate_type]))
@httpretty.activate @httpretty.activate
def test_course_name_missing(self): def test_course_name_missing(self):
......
...@@ -9,9 +9,12 @@ def generate_sku(product): ...@@ -9,9 +9,12 @@ def generate_sku(product):
""" """
# Note: This currently supports seats. In the future, this should # Note: This currently supports seats. In the future, this should
# be updated to accommodate other product classes. # be updated to accommodate other product classes.
_hash = u' '.join((product.attr.certificate_type.lower(), _hash = u' '.join((
product.attr.course_key.lower(), getattr(product.attr, 'certificate_type', '').lower(),
unicode(product.attr.id_verification_required))) product.attr.course_key.lower(),
unicode(product.attr.id_verification_required)
))
_hash = md5(_hash) _hash = md5(_hash)
_hash = _hash.hexdigest()[-7:] _hash = _hash.hexdigest()[-7:]
return _hash.upper() return _hash.upper()
...@@ -3,6 +3,7 @@ import waffle ...@@ -3,6 +3,7 @@ import waffle
from django.dispatch import receiver from django.dispatch import receiver
from oscar.core.loading import get_class from oscar.core.loading import get_class
from ecommerce.courses.utils import mode_for_seat
from ecommerce.extensions.analytics.utils import is_segment_configured, parse_tracking_context, log_exceptions from ecommerce.extensions.analytics.utils import is_segment_configured, parse_tracking_context, log_exceptions
from ecommerce.notifications.notifications import send_notification from ecommerce.notifications.notifications import send_notification
...@@ -33,7 +34,7 @@ def track_completed_order(sender, order=None, **kwargs): # pylint: disable=unus ...@@ -33,7 +34,7 @@ def track_completed_order(sender, order=None, **kwargs): # pylint: disable=unus
# SKU. Marketing is aware that this approach will not scale once we start selling # SKU. Marketing is aware that this approach will not scale once we start selling
# products other than courses, and will need to change in the future. # products other than courses, and will need to change in the future.
'id': line.partner_sku, 'id': line.partner_sku,
'sku': line.product.attr.certificate_type, 'sku': mode_for_seat(line.product),
'name': line.product.title, 'name': line.product.title,
'price': str(line.line_price_excl_tax), 'price': str(line.line_price_excl_tax),
'quantity': line.quantity, 'quantity': line.quantity,
......
...@@ -228,12 +228,12 @@ class EnrollmentFulfillmentModule(BaseFulfillmentModule): ...@@ -228,12 +228,12 @@ class EnrollmentFulfillmentModule(BaseFulfillmentModule):
try: try:
logger.info('Attempting to revoke fulfillment of Line [%d]...', line.id) logger.info('Attempting to revoke fulfillment of Line [%d]...', line.id)
certificate_type = line.product.attr.certificate_type mode = mode_for_seat(line.product)
course_key = line.product.attr.course_key course_key = line.product.attr.course_key
data = { data = {
'user': line.order.user.username, 'user': line.order.user.username,
'is_active': False, 'is_active': False,
'mode': certificate_type, 'mode': mode,
'course_details': { 'course_details': {
'course_id': course_key, 'course_id': course_key,
}, },
...@@ -248,7 +248,7 @@ class EnrollmentFulfillmentModule(BaseFulfillmentModule): ...@@ -248,7 +248,7 @@ class EnrollmentFulfillmentModule(BaseFulfillmentModule):
order_number=line.order.number, order_number=line.order.number,
product_class=line.product.get_product_class().name, product_class=line.product.get_product_class().name,
course_id=course_key, course_id=course_key,
certificate_type=certificate_type, certificate_type=getattr(line.product.attr, 'certificate_type', ''),
user_id=line.order.user.id user_id=line.order.user.id
) )
......
...@@ -143,7 +143,7 @@ class EnrollmentFulfillmentModuleTests(EnrollmentFulfillmentTestMixin, TestCase) ...@@ -143,7 +143,7 @@ class EnrollmentFulfillmentModuleTests(EnrollmentFulfillmentTestMixin, TestCase)
'INFO', 'INFO',
'line_revoked: certificate_type="{}", course_id="{}", order_line_id="{}", order_number="{}", ' 'line_revoked: certificate_type="{}", course_id="{}", order_line_id="{}", order_number="{}", '
'product_class="{}", user_id="{}"'.format( 'product_class="{}", user_id="{}"'.format(
line.product.attr.certificate_type, getattr(line.product.attr, 'certificate_type', ''),
line.product.attr.course_key, line.product.attr.course_key,
line.id, line.id,
line.order.number, line.order.number,
......
...@@ -98,7 +98,7 @@ class Cybersource(BasePaymentProcessor): ...@@ -98,7 +98,7 @@ class Cybersource(BasePaymentProcessor):
single_seat = self.get_single_seat(basket) single_seat = self.get_single_seat(basket)
if single_seat: if single_seat:
parameters[u'merchant_defined_data1'] = single_seat.attr.course_key parameters[u'merchant_defined_data1'] = single_seat.attr.course_key
parameters[u'merchant_defined_data2'] = single_seat.attr.certificate_type parameters[u'merchant_defined_data2'] = getattr(single_seat.attr, 'certificate_type', '')
# Sign all fields # Sign all fields
signed_field_names = parameters.keys() signed_field_names = parameters.keys()
......
import analytics import analytics
from django.dispatch import receiver, Signal from django.dispatch import receiver, Signal
from ecommerce.courses.utils import mode_for_seat
from ecommerce.extensions.analytics.utils import is_segment_configured, parse_tracking_context, log_exceptions from ecommerce.extensions.analytics.utils import is_segment_configured, parse_tracking_context, log_exceptions
...@@ -34,7 +35,7 @@ def track_completed_refund(sender, refund=None, **kwargs): # pylint: disable=un ...@@ -34,7 +35,7 @@ def track_completed_refund(sender, refund=None, **kwargs): # pylint: disable=un
# SKU. Marketing is aware that this approach will not scale once we start selling # SKU. Marketing is aware that this approach will not scale once we start selling
# products other than courses, and will need to change in the future. # products other than courses, and will need to change in the future.
'id': line.order_line.partner_sku, 'id': line.order_line.partner_sku,
'sku': line.order_line.product.attr.certificate_type, 'sku': mode_for_seat(line.order_line.product),
'name': line.order_line.product.title, 'name': line.order_line.product.title,
'price': str(line.line_credit_excl_tax), 'price': str(line.line_credit_excl_tax),
'quantity': -1 * line.quantity, 'quantity': -1 * line.quantity,
......
...@@ -7,8 +7,10 @@ from oscar.core.loading import get_model ...@@ -7,8 +7,10 @@ from oscar.core.loading import get_model
from oscar.test import factories from oscar.test import factories
from oscar.test.newfactories import UserFactory from oscar.test.newfactories import UserFactory
from ecommerce.courses.models import Course
from ecommerce.extensions.refund.status import REFUND, REFUND_LINE from ecommerce.extensions.refund.status import REFUND, REFUND_LINE
Category = get_model("catalogue", "Category") Category = get_model("catalogue", "Category")
Partner = get_model('partner', 'Partner') Partner = get_model('partner', 'Partner')
Product = get_model("catalogue", "Product") Product = get_model("catalogue", "Product")
...@@ -87,14 +89,21 @@ class CourseFactory(object): ...@@ -87,14 +89,21 @@ class CourseFactory(object):
def add_mode(self, name, price, id_verification_required=False): def add_mode(self, name, price, id_verification_required=False):
parent_product = self._get_parent_seat_product() parent_product = self._get_parent_seat_product()
title = u'{mode_name} Seat in {course_name}'.format(mode_name=name, course_name=self.course_name) certificate_type = Course.certificate_type_for_mode(name)
slug = slugify(u'{course_name}-seat-{mode_name}'.format(course_name=self.course_name, mode_name=name)) title = u'{certificate_type} Seat in {course_name}'.format(
certificate_type=certificate_type,
course_name=self.course_name
)
slug = slugify(u'{course_name}-seat-{certificate_type}'.format(
course_name=self.course_name,
certificate_type=certificate_type
))
child_product, created = Product.objects.get_or_create(parent=parent_product, title=title, slug=slug, child_product, created = Product.objects.get_or_create(parent=parent_product, title=title, slug=slug,
structure='child') structure='child')
if created: if created:
child_product.attr.course_key = self.course_id child_product.attr.course_key = self.course_id
child_product.attr.certificate_type = name child_product.attr.certificate_type = certificate_type
child_product.attr.id_verification_required = id_verification_required child_product.attr.id_verification_required = id_verification_required
child_product.save() child_product.save()
......
...@@ -12,6 +12,7 @@ from mock import patch ...@@ -12,6 +12,7 @@ from mock import patch
from oscar.test import factories from oscar.test import factories
from oscar.core.loading import get_model, get_class from oscar.core.loading import get_model, get_class
from ecommerce.courses.utils import mode_for_seat
from ecommerce.extensions.api.constants import APIConstants as AC from ecommerce.extensions.api.constants import APIConstants as AC
from ecommerce.extensions.fulfillment.mixins import FulfillmentMixin from ecommerce.extensions.fulfillment.mixins import FulfillmentMixin
...@@ -201,7 +202,7 @@ class BusinessIntelligenceMixin(object): ...@@ -201,7 +202,7 @@ class BusinessIntelligenceMixin(object):
self.assertEqual(line.product.title, tracked_product['name']) self.assertEqual(line.product.title, tracked_product['name'])
self.assertEqual(str(line.line_price_excl_tax), tracked_product['price']) self.assertEqual(str(line.line_price_excl_tax), tracked_product['price'])
self.assertEqual(line.quantity, tracked_product['quantity']) self.assertEqual(line.quantity, tracked_product['quantity'])
self.assertEqual(line.product.attr.certificate_type, tracked_product['sku']) self.assertEqual(mode_for_seat(line.product), tracked_product['sku'])
self.assertEqual(line.product.get_product_class().name, tracked_product['category']) self.assertEqual(line.product.get_product_class().name, tracked_product['category'])
elif model_name == 'Refund': elif model_name == 'Refund':
self.assertEqual(event_payload['total'], '-{}'.format(total)) self.assertEqual(event_payload['total'], '-{}'.format(total))
...@@ -212,7 +213,7 @@ class BusinessIntelligenceMixin(object): ...@@ -212,7 +213,7 @@ class BusinessIntelligenceMixin(object):
self.assertEqual(line.order_line.product.title, tracked_product['name']) self.assertEqual(line.order_line.product.title, tracked_product['name'])
self.assertEqual(str(line.line_credit_excl_tax), tracked_product['price']) self.assertEqual(str(line.line_credit_excl_tax), tracked_product['price'])
self.assertEqual(-1 * line.quantity, tracked_product['quantity']) self.assertEqual(-1 * line.quantity, tracked_product['quantity'])
self.assertEqual(line.order_line.product.attr.certificate_type, tracked_product['sku']) self.assertEqual(mode_for_seat(line.order_line.product), tracked_product['sku'])
self.assertEqual(line.order_line.product.get_product_class().name, tracked_product['category']) self.assertEqual(line.order_line.product.get_product_class().name, tracked_product['category'])
else: else:
# Payload validation is currently limited to order and refund events # Payload validation is currently limited to order and refund events
......
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