Commit eec2bfbc by Clinton Blackburn

Merge pull request #243 from edx/clintonb/course-api-nested-products

Added optional nesting of course products
parents 0c375a95 3a37f80f
......@@ -123,9 +123,17 @@ class RefundSerializer(serializers.ModelSerializer):
class CourseSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.RegexField(COURSE_ID_REGEX, max_length=255)
products = ProductSerializer(many=True)
products_url = serializers.SerializerMethodField()
last_edited = serializers.SerializerMethodField()
def __init__(self, *args, **kwargs):
super(CourseSerializer, self).__init__(*args, **kwargs)
include_products = kwargs['context'].pop('include_products', False)
if not include_products:
self.fields.pop('products', None)
def get_last_edited(self, obj):
return obj.history.latest().history_date.strftime(ISO_8601_FORMAT)
......@@ -135,8 +143,8 @@ class CourseSerializer(serializers.HyperlinkedModelSerializer):
class Meta(object):
model = Course
fields = ('id', 'url', 'name', 'verification_deadline', 'type', 'products_url', 'last_edited')
read_only_fields = ('type',)
fields = ('id', 'url', 'name', 'verification_deadline', 'type', 'products_url', 'last_edited', 'products')
read_only_fields = ('type', 'products')
extra_kwargs = {
'url': {'view_name': COURSE_DETAIL_VIEW}
}
......
from django.core.urlresolvers import reverse
from django.test import RequestFactory
from oscar.core.loading import get_class
from oscar.test import factories
from oscar.test.newfactories import ProductAttributeValueFactory
from ecommerce.core.constants import ISO_8601_FORMAT
from ecommerce.extensions.api.serializers import OrderSerializer
from ecommerce.tests.mixins import UserMixin, ThrottlingMixin
JSON_CONTENT_TYPE = 'application/json'
Selector = get_class('partner.strategy', 'Selector')
class OrderDetailViewTestMixin(ThrottlingMixin, UserMixin):
......@@ -42,3 +46,26 @@ class TestServerUrlMixin(object):
def get_full_url(self, path):
""" Returns a complete URL with the given path. """
return 'http://testserver' + path
class ProductSerializerMixin(TestServerUrlMixin):
def serialize_product(self, product):
""" Serializes a Product to a Python dict. """
attribute_values = [{'name': av.attribute.name, 'value': av.value} for av in product.attribute_values.all()]
data = {
'id': product.id,
'url': self.get_full_url(reverse('api:v2:product-detail', kwargs={'pk': product.id})),
'structure': product.structure,
'product_class': unicode(product.get_product_class()),
'title': product.title,
'expires': product.expires.strftime(ISO_8601_FORMAT) if product.expires else None,
'attribute_values': attribute_values
}
info = Selector().strategy().fetch_for_product(product)
data.update({
'is_available_to_buy': info.availability.is_available_to_buy,
'price': "{0:.2f}".format(info.price.excl_tax) if info.availability.is_available_to_buy else None
})
return data
......@@ -10,7 +10,7 @@ from waffle import Switch
from ecommerce.core.constants import ISO_8601_FORMAT
from ecommerce.courses.models import Course
from ecommerce.courses.publishers import LMSPublisher
from ecommerce.extensions.api.v2.tests.views import JSON_CONTENT_TYPE, TestServerUrlMixin
from ecommerce.extensions.api.v2.tests.views import JSON_CONTENT_TYPE, ProductSerializerMixin
from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin
from ecommerce.tests.mixins import UserMixin
......@@ -19,7 +19,7 @@ ProductClass = get_model('catalogue', 'ProductClass')
Selector = get_class('partner.strategy', 'Selector')
class CourseViewSetTests(TestServerUrlMixin, CourseCatalogTestMixin, UserMixin, TestCase):
class CourseViewSetTests(ProductSerializerMixin, CourseCatalogTestMixin, UserMixin, TestCase):
maxDiff = None
list_path = reverse('api:v2:course-list')
......@@ -32,14 +32,14 @@ class CourseViewSetTests(TestServerUrlMixin, CourseCatalogTestMixin, UserMixin,
def create_course(self):
return Course.objects.create(id='edX/DemoX/Demo_Course', name='Test Course')
def serialize_course(self, course):
def serialize_course(self, course, include_products=False):
""" Serializes a course to a Python dict. """
products_url = self.get_full_url(reverse('api:v2:course-product-list',
kwargs={'parent_lookup_course_id': course.id}))
last_edited = course.history.latest().history_date.strftime(ISO_8601_FORMAT)
return {
data = {
'id': course.id,
'name': course.name,
'verification_deadline': course.verification_deadline,
......@@ -49,6 +49,11 @@ class CourseViewSetTests(TestServerUrlMixin, CourseCatalogTestMixin, UserMixin,
'last_edited': last_edited
}
if include_products:
data['products'] = [self.serialize_product(product) for product in course.products.all()]
return data
def test_list(self):
""" Verify the view returns a list of Courses. """
response = self.client.get(self.list_path)
......@@ -100,6 +105,11 @@ class CourseViewSetTests(TestServerUrlMixin, CourseCatalogTestMixin, UserMixin,
self.assertEqual(response.status_code, 200)
self.assertDictEqual(json.loads(response.content), self.serialize_course(self.course))
# Verify nested products can be included
response = self.client.get(path + '?include_products=true')
self.assertEqual(response.status_code, 200)
self.assertDictEqual(json.loads(response.content), self.serialize_course(self.course, include_products=True))
def test_update(self):
""" Verify the view updates the information of existing courses. """
course_id = self.course.id
......
......@@ -4,21 +4,19 @@ import json
from django.core.urlresolvers import reverse
from django.test import TestCase
from oscar.core.loading import get_model, get_class
from oscar.core.loading import get_model
import pytz
from ecommerce.core.constants import ISO_8601_FORMAT
from ecommerce.courses.models import Course
from ecommerce.extensions.api.v2.tests.views import JSON_CONTENT_TYPE, TestServerUrlMixin
from ecommerce.extensions.api.v2.tests.views import JSON_CONTENT_TYPE, ProductSerializerMixin
from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin
from ecommerce.tests.mixins import UserMixin
Product = get_model('catalogue', 'Product')
ProductClass = get_model('catalogue', 'ProductClass')
Selector = get_class('partner.strategy', 'Selector')
class ProductViewSetTests(TestServerUrlMixin, CourseCatalogTestMixin, UserMixin, TestCase):
class ProductViewSetTests(ProductSerializerMixin, CourseCatalogTestMixin, UserMixin, TestCase):
maxDiff = None
def setUp(self):
......@@ -31,27 +29,6 @@ class ProductViewSetTests(TestServerUrlMixin, CourseCatalogTestMixin, UserMixin,
expires = datetime.datetime(2100, 1, 1, tzinfo=pytz.UTC)
self.seat = self.course.create_or_update_seat('honor', False, 0, expires=expires)
def serialize_product(self, product):
""" Serializes a Product to a Python dict. """
attribute_values = [{'name': av.attribute.name, 'value': av.value} for av in product.attribute_values.all()]
data = {
'id': product.id,
'url': self.get_full_url(reverse('api:v2:product-detail', kwargs={'pk': product.id})),
'structure': product.structure,
'product_class': unicode(product.get_product_class()),
'title': product.title,
'expires': product.expires.strftime(ISO_8601_FORMAT) if product.expires else None,
'attribute_values': attribute_values
}
info = Selector().strategy().fetch_for_product(product)
data.update({
'is_available_to_buy': info.availability.is_available_to_buy,
'price': "{0:.2f}".format(info.price.excl_tax) if info.availability.is_available_to_buy else None
})
return data
def test_list(self):
""" Verify a list of products is returned. """
path = reverse('api:v2:product-list')
......
......@@ -500,6 +500,11 @@ class CourseViewSet(NonDestroyableModelViewSet):
serializer_class = serializers.CourseSerializer
permission_classes = (IsAuthenticated, IsAdminUser,)
def get_serializer_context(self):
context = super(CourseViewSet, self).get_serializer_context()
context['include_products'] = bool(self.request.GET.get('include_products', False))
return context
@detail_route(methods=['post'])
def publish(self, request, pk=None): # pylint: disable=unused-argument
""" Publish the course to LMS. """
......
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