Commit 91bdf0e6 by Awais

ECOM-3739

Creating end-point for Affiliate datafeed window.
parent ef7d6985
from rest_framework_xml.renderers import XMLRenderer
class AffiliateWindowXMLRenderer(XMLRenderer):
""" XML renderer for Affiliate Window product feed.
Note:
See http://wiki.affiliatewindow.com/index.php/Product_Feed_Building for the complete spec.
"""
item_tag_name = 'product'
root_tag_name = 'merchant'
......@@ -154,3 +154,31 @@ class ContainedCoursesSerializer(serializers.Serializer): # pylint: disable=abs
child=serializers.BooleanField(),
help_text=_('Dictionary mapping course IDs to boolean values')
)
class AffiliateWindowSerializer(serializers.ModelSerializer):
pid = serializers.SerializerMethodField()
name = serializers.CharField(source='course_run.course.title')
desc = serializers.CharField(source='course_run.course.short_description')
purl = serializers.CharField(source='course_run.course.marketing_url')
imgurl = serializers.CharField(source='course_run.image')
category = serializers.SerializerMethodField()
price = serializers.SerializerMethodField()
class Meta(object):
model = Seat
fields = (
'name', 'pid', 'desc', 'category', 'purl', 'imgurl', 'price', 'currency'
)
def get_pid(self, obj):
return '{}-{}'.format(obj.course_run.key, obj.type)
def get_price(self, obj):
return {
'actualp': obj.price
}
def get_category(self, obj): # pylint: disable=unused-argument
# Using hardcoded value for category. This value comes from an Affiliate Window taxonomy.
return 'Other Experiences'
......@@ -6,7 +6,7 @@ from django.test import TestCase
from course_discovery.apps.api.serializers import(
CatalogSerializer, CourseSerializer, CourseRunSerializer, ContainedCoursesSerializer, ImageSerializer,
SubjectSerializer, PrerequisiteSerializer, VideoSerializer, OrganizationSerializer, SeatSerializer,
PersonSerializer,
PersonSerializer, AffiliateWindowSerializer
)
from course_discovery.apps.catalogs.tests.factories import CatalogFactory
from course_discovery.apps.core.tests.factories import UserFactory
......@@ -215,3 +215,26 @@ class PersonSerializerTests(TestCase):
}
self.assertDictEqual(serializer.data, expected)
class AffiliateWindowSerializerTests(TestCase):
def test_data(self):
user = UserFactory()
CatalogFactory(query='*:*', viewers=[user])
course_run = CourseRunFactory()
seat = SeatFactory(course_run=course_run)
serializer = AffiliateWindowSerializer(seat)
expected = {
'pid': '{}-{}'.format(course_run.key, seat.type),
'name': course_run.course.title,
'desc': course_run.course.short_description,
'purl': course_run.course.marketing_url,
'price': {
'actualp': seat.price
},
'currency': seat.currency.code,
'imgurl': course_run.image.src,
'category': 'Other Experiences'
}
self.assertDictEqual(serializer.data, expected)
# pylint: disable=redefined-builtin,no-member
import datetime
from os.path import abspath, join, dirname
import xml.etree.ElementTree as ET
import ddt
import pytz
from lxml import etree
from rest_framework.reverse import reverse
from rest_framework.test import APITestCase
from course_discovery.apps.api.v1.tests.test_views.mixins import SerializationMixin
from course_discovery.apps.catalogs.tests.factories import CatalogFactory
from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.core.tests.mixins import ElasticsearchTestMixin
from course_discovery.apps.course_metadata.models import Seat
from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory, SeatFactory
@ddt.ddt
class AffiliateWindowViewSetTests(ElasticsearchTestMixin, SerializationMixin, APITestCase):
""" Tests for the AffiliateWindowViewSet. """
def setUp(self):
super(AffiliateWindowViewSetTests, self).setUp()
self.user = UserFactory()
self.client.force_authenticate(self.user)
self.catalog = CatalogFactory(query='*:*', viewers=[self.user])
self.enrollment_end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=30)
self.course_run = CourseRunFactory(enrollment_end=self.enrollment_end)
self.seat_verified = SeatFactory(course_run=self.course_run, type=Seat.VERIFIED)
self.course = self.course_run.course
self.affiliate_url = reverse('api:v1:partners:affiliate_window-detail', kwargs={'pk': self.catalog.id})
self.affiliate_window_category = 'Other Experiences'
self.refresh_index()
def test_without_authentication(self):
""" Verify authentication is required when accessing the endpoint. """
self.client.logout()
response = self.client.get(self.affiliate_url)
self.assertEqual(response.status_code, 403)
def test_affiliate_with_supported_seats(self):
""" Verify that endpoint returns course runs for verified and professional seats only. """
response = self.client.get(self.affiliate_url)
self.assertEqual(response.status_code, 200)
root = ET.fromstring(response.content)
self.assertEqual(1, len(root.findall('product')))
self.assert_product_xml(
root.findall('product/[pid="{}-{}"]'.format(self.course_run.key, self.seat_verified.type))[0],
self.seat_verified
)
# Add professional seat.
seat_professional = SeatFactory(course_run=self.course_run, type=Seat.PROFESSIONAL)
response = self.client.get(self.affiliate_url)
root = ET.fromstring(response.content)
self.assertEqual(2, len(root.findall('product')))
self.assert_product_xml(
root.findall('product/[pid="{}-{}"]'.format(self.course_run.key, self.seat_verified.type))[0],
self.seat_verified
)
self.assert_product_xml(
root.findall('product/[pid="{}-{}"]'.format(self.course_run.key, seat_professional.type))[0],
seat_professional
)
@ddt.data(Seat.CREDIT, Seat.HONOR, Seat.AUDIT)
def test_with_non_supported_seats(self, non_supporting_seat):
""" Verify that endpoint returns no data for honor, credit and audit seats. """
self.seat_verified.type = non_supporting_seat
self.seat_verified.save()
response = self.client.get(self.affiliate_url)
self.assertEqual(response.status_code, 200)
root = ET.fromstring(response.content)
self.assertEqual(0, len(root.findall('product')))
def test_with_closed_enrollment(self):
""" Verify that endpoint returns no data if enrollment is close. """
self.course_run.enrollment_end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=-100)
self.course_run.save()
response = self.client.get(self.affiliate_url)
self.assertEqual(response.status_code, 200)
root = ET.fromstring(response.content)
self.assertEqual(0, len(root.findall('product')))
def assert_product_xml(self, content, seat):
""" Helper method to verify product data in xml format. """
self.assertEqual(content.find('pid').text, '{}-{}'.format(self.course_run.key, seat.type))
self.assertEqual(content.find('name').text, self.course_run.course.title)
self.assertEqual(content.find('desc').text, self.course_run.course.short_description)
self.assertEqual(content.find('purl').text, self.course_run.course.marketing_url)
self.assertEqual(content.find('imgurl').text, self.course_run.image.src)
self.assertEqual(content.find('price/actualp').text, str(seat.price))
self.assertEqual(content.find('currency').text, seat.currency.code)
self.assertEqual(content.find('category').text, self.affiliate_window_category)
def test_dtd_with_valid_data(self):
""" Verify the XML data produced by the endpoint conforms to the DTD file. """
response = self.client.get(self.affiliate_url)
self.assertEqual(response.status_code, 200)
filename = abspath(join(dirname(dirname(__file__)), 'affiliate_window_product_feed.1.4.dtd'))
dtd = etree.DTD(open(filename))
root = etree.XML(response.content)
self.assertTrue(dtd.validate(root))
""" API v1 URLs. """
from rest_framework import routers
from django.conf.urls import include, url
from course_discovery.apps.api.v1 import views
urlpatterns = []
partners_router = routers.SimpleRouter()
partners_router.register(r'affiliate_window/catalogs', views.AffiliateWindowViewSet, base_name='affiliate_window')
partners_urls = partners_router.urls
urlpatterns = [
url(r'^partners/', include(partners_urls, namespace='partners')),
]
router = routers.SimpleRouter()
router.register(r'catalogs', views.CatalogViewSet)
......
......@@ -4,6 +4,7 @@ from io import StringIO
from django.core.management import call_command
from django.db.models.functions import Lower
from django.shortcuts import get_object_or_404
from dry_rest_permissions.generics import DRYPermissions
from edx_rest_framework_extensions.permissions import IsSuperuser
from rest_framework import viewsets
......@@ -14,11 +15,12 @@ from rest_framework.response import Response
from course_discovery.apps.api.filters import PermissionsFilter
from course_discovery.apps.api.serializers import (
CatalogSerializer, CourseSerializer, CourseRunSerializer, ContainedCoursesSerializer,
CourseSerializerExcludingClosedRuns,
CourseSerializerExcludingClosedRuns, AffiliateWindowSerializer
)
from course_discovery.apps.api.renderers import AffiliateWindowXMLRenderer
from course_discovery.apps.catalogs.models import Catalog
from course_discovery.apps.course_metadata.constants import COURSE_ID_REGEX, COURSE_RUN_ID_REGEX
from course_discovery.apps.course_metadata.models import Course, CourseRun
from course_discovery.apps.course_metadata.models import Course, CourseRun, Seat
logger = logging.getLogger(__name__)
......@@ -202,3 +204,27 @@ class ManagementViewSet(viewsets.ViewSet):
log=log.getvalue())
return Response(output, content_type='text/plain')
class AffiliateWindowViewSet(viewsets.ViewSet):
""" AffiliateWindow Resource. """
permission_classes = (IsAuthenticated,)
renderer_classes = (AffiliateWindowXMLRenderer,)
serializer_class = AffiliateWindowSerializer
def retrieve(self, request, pk=None): # pylint: disable=redefined-builtin,unused-argument
"""
Return verified and professional seats of courses against provided catalog id.
---
produces:
- application/xml
"""
catalog = get_object_or_404(Catalog, pk=pk)
queryset = catalog.courses().active()
seats = Seat.objects.filter(
course_run__course__in=queryset, type__in=[Seat.VERIFIED, Seat.PROFESSIONAL]
)
serializer = AffiliateWindowSerializer(seats, many=True)
return Response(serializer.data)
......@@ -261,6 +261,7 @@ REST_FRAMEWORK = {
'rest_framework.authentication.SessionAuthentication',
'edx_rest_framework_extensions.authentication.BearerAuthentication',
'edx_rest_framework_extensions.authentication.JwtAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'DEFAULT_PERMISSION_CLASSES': (
......
......@@ -8,6 +8,7 @@ django-sortedm2m==1.1.1
django-waffle==0.11
djangorestframework==3.3.3
djangorestframework-jwt==1.7.2
djangorestframework-xml==1.3.0
django-rest-swagger[reST]==0.3.5
dry-rest-permissions==0.1.6
edx-auth-backends==0.2.3
......
......@@ -6,6 +6,7 @@ ddt==1.0.1
django-nose==1.4.2
edx-lint==0.5.0
factory-boy==2.6.0
lxml==3.4.2
mock==1.3.0
nose-ignore-docstring==0.2
pep8==1.6.2
......
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