Commit 8e335a4c by McKenzie Welter Committed by McKenzie Welter

update entitlement product data loading from ecommerce

parent e888d224
...@@ -311,7 +311,13 @@ class EcommerceApiDataLoader(AbstractDataLoader): ...@@ -311,7 +311,13 @@ class EcommerceApiDataLoader(AbstractDataLoader):
for body in results: for body in results:
body = self.clean_strings(body) body = self.clean_strings(body)
skus.append(self.update_entitlement(body)) skus.append(self.update_entitlement(body))
CourseEntitlement.objects.exclude(sku__in=skus).delete() entitlements_to_delete = CourseEntitlement.objects.filter(partner=self.partner).exclude(sku__in=skus)
for entitlement in entitlements_to_delete:
msg = 'Deleting entitlement with sku {sku} for partner {partner}'.format(
sku=entitlement.sku, partner=entitlement.partner
)
logger.info(msg)
entitlements_to_delete.delete()
def update_seats(self, body): def update_seats(self, body):
course_run_key = body['id'] course_run_key = body['id']
...@@ -366,12 +372,6 @@ class EcommerceApiDataLoader(AbstractDataLoader): ...@@ -366,12 +372,6 @@ class EcommerceApiDataLoader(AbstractDataLoader):
def update_entitlement(self, body): def update_entitlement(self, body):
attributes = {attribute['name']: attribute['value'] for attribute in body['attribute_values']} attributes = {attribute['name']: attribute['value'] for attribute in body['attribute_values']}
course_uuid = attributes.get('UUID') course_uuid = attributes.get('UUID')
try:
course = Course.objects.get(uuid=course_uuid)
except Course.DoesNotExist:
msg = 'Could not find course {uuid}'.format(uuid=course_uuid)
logger.warning(msg)
return None
stock_record = body['stockrecords'][0] stock_record = body['stockrecords'][0]
currency_code = stock_record['price_currency'] currency_code = stock_record['price_currency']
...@@ -379,26 +379,44 @@ class EcommerceApiDataLoader(AbstractDataLoader): ...@@ -379,26 +379,44 @@ class EcommerceApiDataLoader(AbstractDataLoader):
sku = stock_record['partner_sku'] sku = stock_record['partner_sku']
try: try:
course = Course.objects.get(uuid=course_uuid)
except Course.DoesNotExist:
msg = 'Could not find course {uuid} while loading entitlement {entitlement} with sku {sku}'.format(
uuid=course_uuid, entitlement=body['title'], sku=sku
)
logger.warning(msg)
return None
try:
currency = Currency.objects.get(code=currency_code) currency = Currency.objects.get(code=currency_code)
except Currency.DoesNotExist: except Currency.DoesNotExist:
msg = 'Could not find currency {code}'.format(code=currency_code) msg = 'Could not find currency {code} while loading entitlement {entitlement} with sku {sku}'.format(
code=currency_code, entitlement=body['title'], sku=sku
)
logger.warning(msg) logger.warning(msg)
return None return None
mode_name = attributes.get('certificate_type') mode_name = attributes.get('certificate_type')
try: try:
mode = SeatType.objects.get(name=mode_name) mode = SeatType.objects.get(slug=mode_name)
except SeatType.DoesNotExist: except SeatType.DoesNotExist:
msg = 'Could not find course entitlement mode {mode}'.format(mode=mode_name) msg = 'Could not find mode {mode} while loading entitlement {entitlement} with sku {sku}'.format(
mode=mode_name, entitlement=body['title'], sku=sku
)
logger.warning(msg) logger.warning(msg)
return None return None
defaults = { defaults = {
'partner': self.partner,
'price': price, 'price': price,
'currency': currency, 'currency': currency,
'sku': sku, 'sku': sku,
'expires': self.parse_date(body['expires']) 'expires': self.parse_date(body['expires'])
} }
msg = 'Creating entitlement {entitlement} with sku {sku} for partner {partner}'.format(
entitlement=body['title'], sku=sku, partner=self.partner
)
logger.info(msg)
course.entitlements.update_or_create(mode=mode, defaults=defaults) course.entitlements.update_or_create(mode=mode, defaults=defaults)
return sku return sku
......
...@@ -18,7 +18,8 @@ from course_discovery.apps.course_metadata.models import ( ...@@ -18,7 +18,8 @@ from course_discovery.apps.course_metadata.models import (
Course, CourseEntitlement, CourseRun, Organization, Program, ProgramType, Seat, SeatType Course, CourseEntitlement, CourseRun, Organization, Program, ProgramType, Seat, SeatType
) )
from course_discovery.apps.course_metadata.tests.factories import ( from course_discovery.apps.course_metadata.tests.factories import (
CourseFactory, CourseRunFactory, ImageFactory, OrganizationFactory, SeatFactory, VideoFactory CourseEntitlementFactory, CourseFactory, CourseRunFactory, ImageFactory, OrganizationFactory, SeatFactory,
VideoFactory
) )
LOGGER_PATH = 'course_discovery.apps.course_metadata.data_loaders.api.logger' LOGGER_PATH = 'course_discovery.apps.course_metadata.data_loaders.api.logger'
...@@ -371,6 +372,7 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC ...@@ -371,6 +372,7 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC
{ {
"structure": "child", "structure": "child",
"product_class": "Course Entitlement", "product_class": "Course Entitlement",
"title": "Course Intro to Everything",
"price": "10.00", "price": "10.00",
"expires": None, "expires": None,
"attribute_values": [ "attribute_values": [
...@@ -386,9 +388,9 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC ...@@ -386,9 +388,9 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC
"is_available_to_buy": True, "is_available_to_buy": True,
"stockrecords": [ "stockrecords": [
{ {
"partner_sku": "sku132",
"price_currency": alt_currency if alt_currency else "USD", "price_currency": alt_currency if alt_currency else "USD",
"price_excl_tax": "10.00", "price_excl_tax": "10.00",
"partner_sku": "sku132",
} }
] ]
} }
...@@ -402,6 +404,17 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC ...@@ -402,6 +404,17 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC
) )
return bodies return bodies
def compose_warning_log(self, alt_course, alt_currency, alt_mode):
msg = 'Could not find '
if alt_course:
msg += 'course ' + alt_course
elif alt_currency:
msg += 'currency ' + alt_currency
elif alt_mode:
msg += 'mode ' + alt_mode
msg += ' while loading entitlement Course Intro to Everything with sku sku132'
return msg
def assert_seats_loaded(self, body): def assert_seats_loaded(self, body):
""" Assert a Seat corresponding to the specified data body was properly loaded into the database. """ """ Assert a Seat corresponding to the specified data body was properly loaded into the database. """
course_run = CourseRun.objects.get(key=body['id']) course_run = CourseRun.objects.get(key=body['id'])
...@@ -463,7 +476,6 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC ...@@ -463,7 +476,6 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC
self.assertEqual(entitlement.expires, expires) self.assertEqual(entitlement.expires, expires)
self.assertEqual(entitlement.course, course) self.assertEqual(entitlement.course, course)
self.assertEqual(entitlement.mode, mode)
self.assertEqual(entitlement.price, price) self.assertEqual(entitlement.price, price)
self.assertEqual(entitlement.currency.code, price_currency) self.assertEqual(entitlement.currency.code, price_currency)
self.assertEqual(entitlement.sku, sku) self.assertEqual(entitlement.sku, sku)
...@@ -474,9 +486,9 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC ...@@ -474,9 +486,9 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC
courses_api_data = self.mock_courses_api() courses_api_data = self.mock_courses_api()
loaded_course_run_data = courses_api_data[:-1] loaded_course_run_data = courses_api_data[:-1]
loaded_seat_data = courses_api_data[:-2] loaded_seat_data = courses_api_data[:-2]
self.assertEqual(CourseRun.objects.count(), len(loaded_course_run_data))
products_api_data = self.mock_products_api() products_api_data = self.mock_products_api()
self.assertEqual(CourseRun.objects.count(), len(loaded_course_run_data))
# Verify a seat exists on all courses already # Verify a seat exists on all courses already
for course_run in CourseRun.objects.all(): for course_run in CourseRun.objects.all():
...@@ -496,10 +508,27 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC ...@@ -496,10 +508,27 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC
self.loader.ingest() self.loader.ingest()
@responses.activate @responses.activate
@mock.patch(LOGGER_PATH)
def test_ingest_deletes(self, mock_logger):
""" Verifiy the method deletes stale data. """
self.mock_courses_api()
products_api_data = self.mock_products_api()
entitlement = CourseEntitlementFactory(partner=self.partner)
self.loader.ingest()
# Ensure that only entitlements retrieved from the Ecommerce API remain in Discovery,
# and that the sku and partner of the deleted entitlement are logged
self.assert_entitlements_loaded(products_api_data)
msg = 'Deleting entitlement with sku {sku} for partner {partner}'.format(
sku=entitlement.sku, partner=entitlement.partner
)
mock_logger.info.assert_any_call(msg)
@responses.activate
@ddt.data( @ddt.data(
('a01354b1-c0de-4a6b-c5de-ab5c6d869e76', None, None), ('a01354b1-c0de-4a6b-c5de-ab5c6d869e76', None, None),
(None, "NRC", None), (None, "NRC", None),
(None, None, "notamode") (None, None, "notamode"),
) )
@ddt.unpack @ddt.unpack
def test_ingest_fails(self, alt_course, alt_currency, alt_mode): def test_ingest_fails(self, alt_course, alt_currency, alt_mode):
...@@ -508,13 +537,7 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC ...@@ -508,13 +537,7 @@ class EcommerceApiDataLoaderTests(ApiClientTestMixin, DataLoaderTestMixin, TestC
self.mock_products_api(alt_course=alt_course, alt_currency=alt_currency, alt_mode=alt_mode) self.mock_products_api(alt_course=alt_course, alt_currency=alt_currency, alt_mode=alt_mode)
with mock.patch(LOGGER_PATH) as mock_logger: with mock.patch(LOGGER_PATH) as mock_logger:
self.loader.ingest() self.loader.ingest()
msg = 'Could not find ' msg = self.compose_warning_log(alt_course, alt_currency, alt_mode)
if alt_course:
msg += 'course ' + alt_course
elif alt_currency:
msg += 'currency ' + alt_currency
else:
msg += 'course entitlement mode ' + alt_mode
mock_logger.warning.assert_called_with(msg) mock_logger.warning.assert_called_with(msg)
@ddt.unpack @ddt.unpack
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-12-07 19:07
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0007_auto_20171004_1133'),
('course_metadata', '0071_auto_20171128_1945'),
]
operations = [
migrations.AddField(
model_name='courseentitlement',
name='partner',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='core.Partner'),
),
]
...@@ -839,6 +839,7 @@ class CourseEntitlement(TimeStampedModel): ...@@ -839,6 +839,7 @@ class CourseEntitlement(TimeStampedModel):
} }
course = models.ForeignKey(Course, related_name='entitlements') course = models.ForeignKey(Course, related_name='entitlements')
mode = models.ForeignKey(SeatType) mode = models.ForeignKey(SeatType)
partner = models.ForeignKey(Partner, null=True, blank=False)
price = models.DecimalField(**PRICE_FIELD_CONFIG) price = models.DecimalField(**PRICE_FIELD_CONFIG)
currency = models.ForeignKey(Currency) currency = models.ForeignKey(Currency)
sku = models.CharField(max_length=128, null=True, blank=True) sku = models.CharField(max_length=128, null=True, blank=True)
......
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