Commit 15d0d26f by Bill DeRusha

Initial course endpoint

parent 1a51d6d9
...@@ -2,7 +2,63 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -2,7 +2,63 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from course_discovery.apps.catalogs.models import Catalog from course_discovery.apps.catalogs.models import Catalog
from course_discovery.apps.course_metadata.models import Course from course_discovery.apps.course_metadata.models import(
Course, Image, Organization, Prerequisite, Subject, Video
)
class TimestampModelSerializer(serializers.ModelSerializer):
modified = serializers.DateTimeField()
class NamedModelSerializer(serializers.ModelSerializer):
name = serializers.CharField()
class Meta(object):
fields = ('name', )
class SubjectSerializer(NamedModelSerializer):
class Meta(NamedModelSerializer.Meta):
model = Subject
class PrerequisiteSerializer(NamedModelSerializer):
class Meta(NamedModelSerializer.Meta):
model = Prerequisite
class MediaSerializer(serializers.ModelSerializer):
src = serializers.CharField()
description = serializers.CharField()
class ImageSerializer(MediaSerializer):
height = serializers.IntegerField()
width = serializers.IntegerField()
class Meta(object):
model = Image
fields = ('src', 'description', 'height', 'width')
class VideoSerializer(MediaSerializer):
image = ImageSerializer()
class Meta(object):
model = Video
fields = ('src', 'description', 'image', )
class OrganizationSerializer(serializers.ModelSerializer):
name = serializers.CharField()
logo_image = ImageSerializer()
description = serializers.CharField()
homepage_url = serializers.CharField()
class Meta(object):
model = Organization
fields = ('name', 'description', 'logo_image', 'homepage_url', )
class CatalogSerializer(serializers.ModelSerializer): class CatalogSerializer(serializers.ModelSerializer):
...@@ -11,13 +67,23 @@ class CatalogSerializer(serializers.ModelSerializer): ...@@ -11,13 +67,23 @@ class CatalogSerializer(serializers.ModelSerializer):
fields = ('id', 'name', 'query', 'courses_count',) fields = ('id', 'name', 'query', 'courses_count',)
class CourseSerializer(serializers.ModelSerializer): class CourseSerializer(TimestampModelSerializer):
key = serializers.CharField() level_type = serializers.SlugRelatedField(read_only=True, slug_field='name')
title = serializers.CharField() subjects = SubjectSerializer(many=True)
prerequisites = PrerequisiteSerializer(many=True)
expected_learning_items = serializers.SlugRelatedField(many=True, read_only=True, slug_field='value')
image = ImageSerializer()
video = VideoSerializer()
owners = OrganizationSerializer(many=True)
sponsors = OrganizationSerializer(many=True)
class Meta(object): class Meta(object):
model = Course model = Course
fields = ('key', 'title',) fields = (
'key', 'title', 'short_description', 'full_description', 'level_type', 'subjects',
'prerequisites', 'expected_learning_items', 'image', 'video', 'owners', 'sponsors',
'modified',
)
class ContainedCoursesSerializer(serializers.Serializer): # pylint: disable=abstract-method class ContainedCoursesSerializer(serializers.Serializer): # pylint: disable=abstract-method
......
from datetime import datetime
import ddt
from django.test import TestCase from django.test import TestCase
from course_discovery.apps.api.serializers import CatalogSerializer, CourseSerializer, ContainedCoursesSerializer from course_discovery.apps.api.serializers import(
CatalogSerializer, CourseSerializer, ContainedCoursesSerializer, ImageSerializer,
SubjectSerializer, PrerequisiteSerializer, VideoSerializer, OrganizationSerializer
)
from course_discovery.apps.catalogs.tests.factories import CatalogFactory from course_discovery.apps.catalogs.tests.factories import CatalogFactory
from course_discovery.apps.course_metadata.tests.factories import CourseFactory from course_discovery.apps.course_metadata.tests.factories import (
CourseFactory, SubjectFactory, PrerequisiteFactory, ImageFactory, VideoFactory, OrganizationFactory
)
class CatalogSerializerTests(TestCase): class CatalogSerializerTests(TestCase):
...@@ -23,12 +31,29 @@ class CatalogSerializerTests(TestCase): ...@@ -23,12 +31,29 @@ class CatalogSerializerTests(TestCase):
class CourseSerializerTests(TestCase): class CourseSerializerTests(TestCase):
def test_data(self): def test_data(self):
course = CourseFactory() course = CourseFactory()
image = course.image
video = course.video
serializer = CourseSerializer(course) serializer = CourseSerializer(course)
# path = reverse('api:v1:course-detail', kwargs={'key': course.key})
# request = RequestFactory().get(path)
# serializer = CourseSerializer(course, context={'request': request})
expected = { expected = {
'key': course.key, 'key': course.key,
'title': course.title, 'title': course.title,
'short_description': course.short_description,
'full_description': course.full_description,
'level_type': course.level_type.name,
'subjects': [],
'prerequisites': [],
'expected_learning_items': [],
'image': ImageSerializer(image).data,
'video': VideoSerializer(video).data,
'owners': [],
'sponsors': [],
'modified': datetime.strftime(course.modified, "%Y-%m-%dT%H:%M:%S.%fZ") # pylint: disable=no-member
} }
self.assertDictEqual(serializer.data, expected) self.assertDictEqual(serializer.data, expected)
...@@ -42,3 +67,72 @@ class ContainedCoursesSerializerTests(TestCase): ...@@ -42,3 +67,72 @@ class ContainedCoursesSerializerTests(TestCase):
} }
serializer = ContainedCoursesSerializer(instance) serializer = ContainedCoursesSerializer(instance)
self.assertDictEqual(serializer.data, instance) self.assertDictEqual(serializer.data, instance)
@ddt.ddt
class LinkObjectSerializerTests(TestCase):
@ddt.data(
(SubjectFactory, SubjectSerializer),
(PrerequisiteFactory, PrerequisiteSerializer),
)
@ddt.unpack
def test_data(self, factory_class, serializer_class):
link_object = factory_class()
serializer = serializer_class(link_object)
expected = {
'name': link_object.name
}
self.assertDictEqual(serializer.data, expected)
class ImageSerializerTests(TestCase):
def test_data(self):
image = ImageFactory()
serializer = ImageSerializer(image)
expected = {
'src': image.src,
'description': image.description,
'height': image.height,
'width': image.width
}
self.assertDictEqual(serializer.data, expected)
class VideoSerializerTests(TestCase):
def test_data(self):
video = VideoFactory()
image = video.image
serializer = VideoSerializer(video)
expected = {
'src': video.src,
'description': video.description,
'image': ImageSerializer(image).data
}
self.assertDictEqual(serializer.data, expected)
class OrganizationSerializerTests(TestCase):
def test_data(self):
organization = OrganizationFactory()
image = organization.logo_image
serializer = OrganizationSerializer(organization)
expected = {
'name': organization.name,
'description': organization.description,
'homepage_url': organization.homepage_url,
'logo_image': {
'src': image.src,
'description': image.description,
'height': image.height,
'width': image.width
}
}
self.assertDictEqual(serializer.data, expected)
# pylint: disable=redefined-builtin
import ddt
import responses
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from course_discovery.apps.api.v1.tests.test_views.mixins import SerializationMixin, OAuth2Mixin from course_discovery.apps.api.v1.tests.test_views.mixins import SerializationMixin
from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD from course_discovery.apps.core.tests.factories import UserFactory, USER_PASSWORD
from course_discovery.apps.core.tests.mixins import ElasticsearchTestMixin
from course_discovery.apps.course_metadata.tests.factories import CourseFactory from course_discovery.apps.course_metadata.tests.factories import CourseFactory
from course_discovery.apps.course_metadata.models import Course
@ddt.ddt class CourseViewSetTests(SerializationMixin, APITestCase):
class CourseViewSetTests(ElasticsearchTestMixin, SerializationMixin, OAuth2Mixin, APITestCase):
def setUp(self): def setUp(self):
super(CourseViewSetTests, self).setUp() super(CourseViewSetTests, self).setUp()
self.user = UserFactory(is_staff=True, is_superuser=True) self.user = UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=self.user.username, password=USER_PASSWORD) self.client.login(username=self.user.username, password=USER_PASSWORD)
self.course = CourseFactory()
@ddt.data('json', 'api') def test_get(self):
def test_list(self, format): """ Verify the endpoint returns the details for a single course. """
""" Verify the endpoint returns a list of all courses. """ url = reverse('api:v1:course-detail', kwargs={'key': self.course.key})
courses = CourseFactory.create_batch(10)
courses.sort(key=lambda course: course.key.lower())
url = reverse('api:v1:course-list')
limit = 3
response = self.client.get(url, {'format': format, 'limit': limit}) response = self.client.get(url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertListEqual(response.data['results'], self.serialize_course(courses[:limit], many=True, format=format)) self.assertEqual(response.data, self.serialize_course(self.course))
response.render()
def test_retrieve(self): def test_list(self):
""" Verify the endpoint returns a single course. """ """ Verify the endpoint returns a list of all catalogs. """
self.assert_retrieve_success() url = reverse('api:v1:course-list')
def assert_retrieve_success(self, **headers): response = self.client.get(url)
""" Asserts the endpoint returns details for a single course. """
course = CourseFactory()
url = reverse('api:v1:course-detail', kwargs={'key': course.key})
response = self.client.get(url, format='json', **headers)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, self.serialize_course(course)) self.assertListEqual(response.data['results'], self.serialize_course(Course.objects.all(), many=True))
@responses.activate
def test_retrieve_with_oauth2_authentication(self):
self.client.logout()
self.mock_user_info_response(self.user)
self.assert_retrieve_success(HTTP_AUTHORIZATION=self.generate_oauth2_token_header(self.user))
...@@ -26,6 +26,7 @@ class CatalogViewSet(viewsets.ModelViewSet): ...@@ -26,6 +26,7 @@ class CatalogViewSet(viewsets.ModelViewSet):
queryset = Catalog.objects.all() queryset = Catalog.objects.all()
serializer_class = CatalogSerializer serializer_class = CatalogSerializer
# The boilerplate methods are required to be recognized by swagger
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
""" Create a new catalog. """ """ Create a new catalog. """
return super(CatalogViewSet, self).create(request, *args, **kwargs) return super(CatalogViewSet, self).create(request, *args, **kwargs)
...@@ -96,10 +97,11 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet): ...@@ -96,10 +97,11 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet):
""" Course resource. """ """ Course resource. """
lookup_field = 'key' lookup_field = 'key'
lookup_value_regex = COURSE_ID_REGEX lookup_value_regex = COURSE_ID_REGEX
queryset = Course.objects.all().order_by(Lower('key'))
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
serializer_class = CourseSerializer serializer_class = CourseSerializer
queryset = Course.objects.all().order_by(Lower('key'))
# The boilerplate methods are required to be recognized by swagger
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
""" List all courses. """ """ List all courses. """
return super(CourseViewSet, self).list(request, *args, **kwargs) return super(CourseViewSet, self).list(request, *args, **kwargs)
......
...@@ -62,6 +62,9 @@ class ExpectedLearningItem(TimeStampedModel): ...@@ -62,6 +62,9 @@ class ExpectedLearningItem(TimeStampedModel):
""" ExpectedLearningItem model. """ """ ExpectedLearningItem model. """
value = models.CharField(max_length=255) value = models.CharField(max_length=255)
def __str__(self):
return self.value
class SyllabusItem(TimeStampedModel): class SyllabusItem(TimeStampedModel):
""" SyllabusItem model. """ """ SyllabusItem model. """
...@@ -117,6 +120,14 @@ class Course(TimeStampedModel): ...@@ -117,6 +120,14 @@ class Course(TimeStampedModel):
history = HistoricalRecords() history = HistoricalRecords()
@property
def owners(self):
return self.organizations.filter(courseorganization__relation_type=CourseOrganization.OWNER)
@property
def sponsors(self):
return self.organizations.filter(courseorganization__relation_type=CourseOrganization.SPONSOR)
def __str__(self): def __str__(self):
return '{key}: {title}'.format(key=self.key, title=self.title) return '{key}: {title}'.format(key=self.key, title=self.title)
......
import factory import factory
from factory.fuzzy import FuzzyText from factory.fuzzy import BaseFuzzyAttribute, FuzzyText, FuzzyChoice
from course_discovery.apps.course_metadata.models import Course, CourseRun, Organization, Person from course_discovery.apps.course_metadata.models import(
Course, CourseRun, Organization, Person, Image, Video, Subject, Prerequisite, LevelType
)
class FuzzyURL(BaseFuzzyAttribute):
def fuzz(self):
protocol = FuzzyChoice(('http', 'https',))
subdomain = FuzzyText()
domain = FuzzyText()
tld = FuzzyChoice(('com', 'net', 'org', 'biz', 'pizza', 'coffee', 'diamonds', 'fail', 'win', 'wtf',))
resource = FuzzyText()
return "{protocol}://{subdomain}.{domain}.{tld}/{resource}".format(
protocol=protocol,
subdomain=subdomain,
domain=domain,
tld=tld,
resource=resource
)
class AbstractMediaModelFactory(factory.DjangoModelFactory):
src = FuzzyURL()
description = FuzzyText()
class AbstractNamedModelFactory(factory.DjangoModelFactory):
name = FuzzyText()
class ImageFactory(AbstractMediaModelFactory):
height = 100
width = 100
class Meta:
model = Image
class VideoFactory(AbstractMediaModelFactory):
image = factory.SubFactory(ImageFactory)
class Meta:
model = Video
class SubjectFactory(AbstractNamedModelFactory):
class Meta:
model = Subject
class LevelTypeFactory(AbstractNamedModelFactory):
class Meta:
model = LevelType
class PrerequisiteFactory(AbstractNamedModelFactory):
class Meta:
model = Prerequisite
class CourseFactory(factory.DjangoModelFactory): class CourseFactory(factory.DjangoModelFactory):
...@@ -9,6 +69,9 @@ class CourseFactory(factory.DjangoModelFactory): ...@@ -9,6 +69,9 @@ class CourseFactory(factory.DjangoModelFactory):
title = FuzzyText(prefix="Test çօմɾʂҽ ") title = FuzzyText(prefix="Test çօմɾʂҽ ")
short_description = FuzzyText(prefix="Test çօմɾʂҽ short description") short_description = FuzzyText(prefix="Test çօմɾʂҽ short description")
full_description = FuzzyText(prefix="Test çօմɾʂҽ FULL description") full_description = FuzzyText(prefix="Test çօմɾʂҽ FULL description")
level_type = factory.SubFactory(LevelTypeFactory)
image = factory.SubFactory(ImageFactory)
video = factory.SubFactory(VideoFactory)
class Meta: class Meta:
model = Course model = Course
...@@ -28,6 +91,9 @@ class CourseRunFactory(factory.DjangoModelFactory): ...@@ -28,6 +91,9 @@ class CourseRunFactory(factory.DjangoModelFactory):
class OrganizationFactory(factory.DjangoModelFactory): class OrganizationFactory(factory.DjangoModelFactory):
key = FuzzyText(prefix='Org.fake/') key = FuzzyText(prefix='Org.fake/')
name = FuzzyText() name = FuzzyText()
description = FuzzyText()
homepage_url = FuzzyURL()
logo_image = factory.SubFactory(ImageFactory)
class Meta: class Meta:
model = Organization model = Organization
......
import ddt import ddt
from django.test import TestCase from django.test import TestCase
from course_discovery.apps.course_metadata.models import AbstractNamedModel, AbstractMediaModel from course_discovery.apps.course_metadata.models import(
AbstractNamedModel, AbstractMediaModel, CourseOrganization, ExpectedLearningItem
)
from course_discovery.apps.course_metadata.tests import factories from course_discovery.apps.course_metadata.tests import factories
class CourseTests(TestCase): class CourseTests(TestCase):
""" Tests for the `Course` model. """ """ Tests for the `Course` model. """
def setUp(self):
super(CourseTests, self).setUp()
self.course = factories.CourseFactory()
self.owner = factories.OrganizationFactory()
self.sponsor = factories.OrganizationFactory()
CourseOrganization.objects.create(
course=self.course,
organization=self.owner,
relation_type=CourseOrganization.OWNER
)
CourseOrganization.objects.create(
course=self.course,
organization=self.sponsor,
relation_type=CourseOrganization.SPONSOR
)
def test_str(self): def test_str(self):
""" Verify casting an instance to a string returns a string containing the key and title. """ """ Verify casting an instance to a string returns a string containing the key and title. """
course = factories.CourseFactory() self.assertEqual(str(self.course), '{key}: {title}'.format(key=self.course.key, title=self.course.title))
self.assertEqual(str(course), '{key}: {title}'.format(key=course.key, title=course.title))
def test_owners(self):
""" Verify that the owners property returns only owner related organizations. """
owners = self.course.owners # pylint: disable=no-member
self.assertEqual(len(owners), 1)
self.assertEqual(owners[0], self.owner)
def test_sponsors(self):
""" Verify that the sponsors property returns only sponsor related organizations. """
sponsors = self.course.sponsors # pylint: disable=no-member
self.assertEqual(len(sponsors), 1)
self.assertEqual(sponsors[0], self.sponsor)
@ddt.ddt @ddt.ddt
...@@ -85,7 +114,7 @@ class AbstractMediaModelTests(TestCase): ...@@ -85,7 +114,7 @@ class AbstractMediaModelTests(TestCase):
""" Tests for AbstractMediaModel. """ """ Tests for AbstractMediaModel. """
def test_str(self): def test_str(self):
""" Verify casting an instance to a string returns a string containing the name. """ """ Verify casting an instance to a string returns a string containing the src. """
class TestAbstractMediaModel(AbstractMediaModel): class TestAbstractMediaModel(AbstractMediaModel):
pass pass
...@@ -93,3 +122,14 @@ class AbstractMediaModelTests(TestCase): ...@@ -93,3 +122,14 @@ class AbstractMediaModelTests(TestCase):
src = 'http://example.com/image.jpg' src = 'http://example.com/image.jpg'
instance = TestAbstractMediaModel(src=src) instance = TestAbstractMediaModel(src=src)
self.assertEqual(str(instance), src) self.assertEqual(str(instance), src)
class ExpectedLearningItemTests(TestCase):
""" Tests for ExpectedLearningItem. """
def test_str(self):
""" Verify casting an instance to a string returns a string containing the value. """
value = 'Expected learnings'
instance = ExpectedLearningItem(value=value)
self.assertEqual(str(instance), value)
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