Commit 229f37ef by Michael Terry Committed by Michael Terry

Publish entitlements along with seats

When publishing a course run to ecommerce and discovery, also push
any entitlements for its course to those same services.

This reverts commit 3db5a5ef which was
itself a reversion to back out these changes while we fixed our staging
services.

LEARNER-4031
parent 5db231a0
import pytest
from course_discovery.apps.core.utils import serialize_datetime
from course_discovery.apps.publisher.api.utils import serialize_seat_for_ecommerce_api
from course_discovery.apps.publisher.api.utils import (serialize_entitlement_for_ecommerce_api,
serialize_seat_for_ecommerce_api)
from course_discovery.apps.publisher.models import Seat
from course_discovery.apps.publisher.tests.factories import SeatFactory
from course_discovery.apps.publisher.tests.factories import CourseEntitlementFactory, SeatFactory
@pytest.mark.django_db
......@@ -50,3 +51,22 @@ class TestSerializeSeatForEcommerceApi:
}
]
assert actual['attribute_values'] == expected_attribute_values
@pytest.mark.django_db
class TestSerializeEntitlementForEcommerceApi:
def test_serialize_entitlement_for_ecommerce_api(self):
entitlement = CourseEntitlementFactory()
actual = serialize_entitlement_for_ecommerce_api(entitlement)
expected = {
'price': str(entitlement.price),
'product_class': 'Course Entitlement',
'attribute_values': [
{
'name': 'certificate_type',
'value': entitlement.mode,
},
]
}
assert actual == expected
......@@ -18,3 +18,16 @@ def serialize_seat_for_ecommerce_api(seat):
}
]
}
def serialize_entitlement_for_ecommerce_api(entitlement):
return {
'price': str(entitlement.price),
'product_class': 'Course Entitlement',
'attribute_values': [
{
'name': 'certificate_type',
'value': entitlement.mode,
},
],
}
......@@ -11,14 +11,17 @@ from course_discovery.apps.core.models import Currency, Partner
from course_discovery.apps.core.tests.factories import StaffUserFactory, UserFactory
from course_discovery.apps.core.tests.helpers import make_image_file
from course_discovery.apps.core.utils import serialize_datetime
from course_discovery.apps.course_metadata.models import CourseEntitlement as DiscoveryCourseEntitlement
from course_discovery.apps.course_metadata.models import Seat as DiscoverySeat
from course_discovery.apps.course_metadata.models import CourseRun, Video
from course_discovery.apps.course_metadata.models import CourseRun, SeatType, Video
from course_discovery.apps.course_metadata.tests import toggle_switch
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory, PersonFactory
from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.api.utils import serialize_seat_for_ecommerce_api
from course_discovery.apps.publisher.api.utils import (serialize_entitlement_for_ecommerce_api,
serialize_seat_for_ecommerce_api)
from course_discovery.apps.publisher.api.v1.views import CourseRunViewSet
from course_discovery.apps.publisher.models import Seat
from course_discovery.apps.publisher.tests.factories import CourseRunFactory, SeatFactory
from course_discovery.apps.publisher.models import CourseEntitlement, Seat
from course_discovery.apps.publisher.tests.factories import CourseEntitlementFactory, CourseRunFactory, SeatFactory
PUBLISHER_UPGRADE_DEADLINE_DAYS = random.randint(1, 21)
......@@ -76,29 +79,23 @@ class CourseRunViewSetTests(APITestCase):
url = '{root}publication/'.format(root=partner.ecommerce_api_url)
responses.add(responses.POST, url, json=body, status=status)
def serialize_seat_for_ecommerce_api(self, seat):
return {
'expires': serialize_datetime(seat.upgrade_deadline or seat.course_run.end),
'price': str(seat.price),
'product_class': 'Seat',
'attribute_values': [
{
'name': 'certificate_type',
'value': None if seat.type is Seat.AUDIT else seat.type,
},
{
'name': 'id_verification_required',
'value': seat.type in (Seat.VERIFIED, Seat.PROFESSIONAL),
}
]
}
@responses.activate
@mock.patch.object(Partner, 'access_token', return_value='JWT fake')
def test_publish(self, mock_access_token): # pylint: disable=unused-argument,too-many-statements
publisher_course_run = self._create_course_run_for_publication()
toggle_switch('publisher_entitlements', True)
publisher_course_run = self._create_course_run_for_publication()
currency = Currency.objects.get(code='USD')
common_entitlement_kwargs = {
'course': publisher_course_run.course,
'currency': currency,
}
professional_entitlement = CourseEntitlementFactory(mode=CourseEntitlement.PROFESSIONAL,
**common_entitlement_kwargs)
verified_entitlement = CourseEntitlementFactory(mode=CourseEntitlement.VERIFIED,
**common_entitlement_kwargs)
common_seat_kwargs = {
'course_run': publisher_course_run,
'currency': currency,
......@@ -132,6 +129,8 @@ class CourseRunViewSetTests(APITestCase):
serialize_seat_for_ecommerce_api(audit_seat),
serialize_seat_for_ecommerce_api(professional_seat),
serialize_seat_for_ecommerce_api(verified_seat),
serialize_entitlement_for_ecommerce_api(professional_entitlement),
serialize_entitlement_for_ecommerce_api(verified_entitlement),
]
assert ecommerce_body['products'] == expected
assert ecommerce_body['verification_deadline'] == serialize_datetime(publisher_course_run.end)
......@@ -178,6 +177,22 @@ class CourseRunViewSetTests(APITestCase):
expected = {publisher_course.primary_subject, publisher_course.secondary_subject}
assert set(discovery_course.subjects.all()) == expected
common_entitlement_kwargs = {
'course': discovery_course,
'currency': currency,
}
self.assertEqual(2, DiscoveryCourseEntitlement.objects.all().count())
DiscoveryCourseEntitlement.objects.get(
mode=SeatType.objects.get(slug=DiscoverySeat.PROFESSIONAL),
price=professional_entitlement.price,
**common_entitlement_kwargs
)
DiscoveryCourseEntitlement.objects.get(
mode=SeatType.objects.get(slug=DiscoverySeat.VERIFIED),
price=verified_entitlement.price,
**common_entitlement_kwargs
)
common_seat_kwargs = {
'course_run': discovery_course_run,
'currency': currency,
......
import logging
from collections import OrderedDict
import waffle
from edx_rest_api_client.client import EdxRestApiClient
from edx_rest_framework_extensions.authentication import JwtAuthentication
from rest_framework import permissions, serializers, status, viewsets
......@@ -10,10 +11,13 @@ from rest_framework.response import Response
from slumber.exceptions import SlumberBaseException
from course_discovery.apps.core.utils import serialize_datetime
from course_discovery.apps.course_metadata.models import CourseEntitlement as DiscoveryCourseEntitlement
from course_discovery.apps.course_metadata.models import CourseRun as DiscoveryCourseRun
from course_discovery.apps.course_metadata.models import Seat as DiscoverySeat
from course_discovery.apps.course_metadata.models import Course, Video
from course_discovery.apps.publisher.api.utils import serialize_seat_for_ecommerce_api
from course_discovery.apps.course_metadata.models import Course, SeatType, Video
from course_discovery.apps.publisher.api.utils import (
serialize_entitlement_for_ecommerce_api, serialize_seat_for_ecommerce_api
)
from course_discovery.apps.publisher.models import CourseRun, Seat
from course_discovery.apps.publisher.studio_api_utils import StudioAPI
......@@ -38,8 +42,9 @@ class CourseRunViewSet(viewsets.GenericViewSet):
try:
publication_status['studio'] = self.publish_to_studio(partner, course_run)
publication_status['ecommerce'] = self.publish_to_ecommerce(partner, course_run)
publication_status['discovery'] = self.publish_to_discovery(partner, course_run)
# ecommerce is going to want to ask discovery about the course's UUID, so we do this last
publication_status['ecommerce'] = self.publish_to_ecommerce(partner, course_run)
except SlumberBaseException as ex:
logger.exception('Failed to publish course run [%s]!', pk)
content = getattr(ex, 'content', None)
......@@ -77,11 +82,16 @@ class CourseRunViewSet(viewsets.GenericViewSet):
'name': course_run.title_override or course_run.course.title,
'verification_deadline': serialize_datetime(course_run.end),
'create_or_activate_enrollment_code': False,
# NOTE (CCB): We only order here to aid testing. The E-Commerce API does NOT care about ordering.
'products': [serialize_seat_for_ecommerce_api(seat) for seat in
course_run.seats.exclude(type=Seat.CREDIT).order_by('created')],
}
# NOTE: We only order here to aid testing. The E-Commerce API does NOT care about ordering.
products = [serialize_seat_for_ecommerce_api(seat) for seat in
course_run.seats.exclude(type=Seat.CREDIT).order_by('created')]
if waffle.switch_is_active('publisher_entitlements'):
products.extend([serialize_entitlement_for_ecommerce_api(entitlement) for entitlement in
course_run.course.entitlements.order_by('created')])
data['products'] = products
try:
api.publication.post(data)
return self.PUBLICATION_SUCCESS_STATUS
......@@ -142,6 +152,18 @@ class CourseRunViewSet(viewsets.GenericViewSet):
discovery_course_run.transcript_languages.add(*course_run.transcript_languages.all())
discovery_course_run.staff.add(*course_run.staff.all())
if waffle.switch_is_active('publisher_entitlements'):
for entitlement in publisher_course.entitlements.all():
DiscoveryCourseEntitlement.objects.update_or_create(
course=discovery_course,
mode=SeatType.objects.get(slug=entitlement.mode),
defaults={
'partner': partner,
'price': entitlement.price,
'currency': entitlement.currency,
}
)
for seat in course_run.seats.exclude(type=Seat.CREDIT):
DiscoverySeat.objects.update_or_create(
course_run=discovery_course_run,
......
......@@ -12,8 +12,9 @@ from course_discovery.apps.course_metadata.choices import CourseRunPacing
from course_discovery.apps.course_metadata.tests import factories
from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.choices import PublisherUserRole
from course_discovery.apps.publisher.models import (Course, CourseRun, CourseRunState, CourseState, CourseUserRole,
OrganizationExtension, OrganizationUserRole, Seat, UserAttributes)
from course_discovery.apps.publisher.models import (Course, CourseEntitlement, CourseRun, CourseRunState, CourseState,
CourseUserRole, OrganizationExtension, OrganizationUserRole, Seat,
UserAttributes)
class CourseFactory(factory.DjangoModelFactory):
......@@ -90,6 +91,16 @@ class SeatFactory(factory.DjangoModelFactory):
model = Seat
class CourseEntitlementFactory(factory.DjangoModelFactory):
mode = FuzzyChoice([name for name, __ in CourseEntitlement.COURSE_MODE_CHOICES])
price = FuzzyDecimal(1.0, 650.0)
currency = factory.Iterator(Currency.objects.all())
course = factory.SubFactory(CourseFactory)
class Meta:
model = CourseEntitlement
class GroupFactory(factory.DjangoModelFactory):
name = FuzzyText()
......
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