Commit 5107ad29 by Mike Dikan

Update to course API to include (condensed) program info

parent 125bf70c
...@@ -189,6 +189,19 @@ class CatalogSerializer(serializers.ModelSerializer): ...@@ -189,6 +189,19 @@ class CatalogSerializer(serializers.ModelSerializer):
fields = ('id', 'name', 'query', 'courses_count', 'viewers') fields = ('id', 'name', 'query', 'courses_count', 'viewers')
class NestedProgramSerializer(serializers.ModelSerializer):
"""
Serializer used when nesting a Program inside another entity (e.g. a Course). The resulting data includes only
the basic details of the Program and none of the details about its related entities (e.g. courses).
"""
type = serializers.SlugRelatedField(slug_field='name', queryset=ProgramType.objects.all())
class Meta:
model = Program
fields = ('uuid', 'title', 'type', 'marketing_slug', 'marketing_url',)
read_only_fields = ('uuid', 'marketing_url',)
class CourseRunSerializer(TimestampModelSerializer): class CourseRunSerializer(TimestampModelSerializer):
"""Serializer for the ``CourseRun`` model.""" """Serializer for the ``CourseRun`` model."""
course = serializers.SlugRelatedField(read_only=True, slug_field='key') course = serializers.SlugRelatedField(read_only=True, slug_field='key')
...@@ -218,6 +231,15 @@ class CourseRunSerializer(TimestampModelSerializer): ...@@ -218,6 +231,15 @@ class CourseRunSerializer(TimestampModelSerializer):
return get_marketing_url_for_user(self.context['request'].user, obj.marketing_url) return get_marketing_url_for_user(self.context['request'].user, obj.marketing_url)
class CourseRunWithProgramsSerializer(CourseRunSerializer):
"""A ``CourseRunSerializer`` which includes programs derived from parent course."""
programs = NestedProgramSerializer(many=True)
class Meta(CourseRunSerializer.Meta):
model = CourseRun
fields = CourseRunSerializer.Meta.fields + ('programs', )
class ContainedCourseRunsSerializer(serializers.Serializer): class ContainedCourseRunsSerializer(serializers.Serializer):
"""Serializer used to represent course runs contained by a catalog.""" """Serializer used to represent course runs contained by a catalog."""
course_runs = serializers.DictField( course_runs = serializers.DictField(
...@@ -244,13 +266,22 @@ class CourseSerializer(TimestampModelSerializer): ...@@ -244,13 +266,22 @@ class CourseSerializer(TimestampModelSerializer):
fields = ( fields = (
'key', 'title', 'short_description', 'full_description', 'level_type', 'subjects', 'prerequisites', 'key', 'title', 'short_description', 'full_description', 'level_type', 'subjects', 'prerequisites',
'expected_learning_items', 'image', 'video', 'owners', 'sponsors', 'modified', 'course_runs', 'expected_learning_items', 'image', 'video', 'owners', 'sponsors', 'modified', 'course_runs',
'marketing_url' 'marketing_url',
) )
def get_marketing_url(self, obj): def get_marketing_url(self, obj):
return get_marketing_url_for_user(self.context['request'].user, obj.marketing_url) return get_marketing_url_for_user(self.context['request'].user, obj.marketing_url)
class CourseWithProgramsSerializer(CourseSerializer):
"""A ``CourseSerializer`` which includes programs."""
programs = NestedProgramSerializer(many=True)
class Meta(CourseSerializer.Meta):
model = Course
fields = CourseSerializer.Meta.fields + ('programs', )
class CourseSerializerExcludingClosedRuns(CourseSerializer): class CourseSerializerExcludingClosedRuns(CourseSerializer):
"""A ``CourseSerializer`` which only includes active course runs, as determined by ``CourseQuerySet``.""" """A ``CourseSerializer`` which only includes active course runs, as determined by ``CourseQuerySet``."""
course_runs = CourseRunSerializer(many=True, source='active_course_runs') course_runs = CourseRunSerializer(many=True, source='active_course_runs')
......
...@@ -11,7 +11,8 @@ from course_discovery.apps.api.serializers import ( ...@@ -11,7 +11,8 @@ from course_discovery.apps.api.serializers import (
CatalogSerializer, CourseSerializer, CourseRunSerializer, ContainedCoursesSerializer, ImageSerializer, CatalogSerializer, CourseSerializer, CourseRunSerializer, ContainedCoursesSerializer, ImageSerializer,
SubjectSerializer, PrerequisiteSerializer, VideoSerializer, OrganizationSerializer, SeatSerializer, SubjectSerializer, PrerequisiteSerializer, VideoSerializer, OrganizationSerializer, SeatSerializer,
PersonSerializer, AffiliateWindowSerializer, ContainedCourseRunsSerializer, CourseRunSearchSerializer, PersonSerializer, AffiliateWindowSerializer, ContainedCourseRunsSerializer, CourseRunSearchSerializer,
ProgramSerializer, ProgramSearchSerializer, ProgramCourseSerializer ProgramSerializer, ProgramSearchSerializer, ProgramCourseSerializer, NestedProgramSerializer,
CourseRunWithProgramsSerializer, CourseWithProgramsSerializer
) )
from course_discovery.apps.catalogs.tests.factories import CatalogFactory from course_discovery.apps.catalogs.tests.factories import CatalogFactory
from course_discovery.apps.core.models import User from course_discovery.apps.core.models import User
...@@ -76,7 +77,7 @@ class CourseSerializerTests(TestCase): ...@@ -76,7 +77,7 @@ class CourseSerializerTests(TestCase):
request = make_request() request = make_request()
CourseRunFactory.create_batch(3, course=course) CourseRunFactory.create_batch(3, course=course)
serializer = CourseSerializer(course, context={'request': request}) serializer = CourseWithProgramsSerializer(course, context={'request': request})
expected = { expected = {
'key': course.key, 'key': course.key,
...@@ -99,7 +100,8 @@ class CourseSerializerTests(TestCase): ...@@ -99,7 +100,8 @@ class CourseSerializerTests(TestCase):
'utm_source': request.user.username, 'utm_source': request.user.username,
'utm_medium': request.user.referral_tracking_id, 'utm_medium': request.user.referral_tracking_id,
}) })
) ),
'programs': NestedProgramSerializer(course.programs, many=True, context={'request': request}).data,
} }
self.assertDictEqual(serializer.data, expected) self.assertDictEqual(serializer.data, expected)
...@@ -119,9 +121,11 @@ class CourseRunSerializerTests(TestCase): ...@@ -119,9 +121,11 @@ class CourseRunSerializerTests(TestCase):
def test_data(self): def test_data(self):
request = make_request() request = make_request()
course_run = CourseRunFactory() course_run = CourseRunFactory()
course = course_run.course
image = course_run.image image = course_run.image
video = course_run.video video = course_run.video
serializer = CourseRunSerializer(course_run, context={'request': request}) serializer = CourseRunWithProgramsSerializer(course_run, context={'request': request})
ProgramFactory(courses=[course])
expected = { expected = {
'course': course_run.course.key, 'course': course_run.course.key,
...@@ -154,6 +158,7 @@ class CourseRunSerializerTests(TestCase): ...@@ -154,6 +158,7 @@ class CourseRunSerializerTests(TestCase):
), ),
'level_type': course_run.level_type.name, 'level_type': course_run.level_type.name,
'availability': course_run.availability, 'availability': course_run.availability,
'programs': NestedProgramSerializer(course.programs, many=True, context={'request': request}).data,
} }
self.assertDictEqual(serializer.data, expected) self.assertDictEqual(serializer.data, expected)
...@@ -239,7 +244,7 @@ class ProgramCourseSerializerTests(TestCase): ...@@ -239,7 +244,7 @@ class ProgramCourseSerializerTests(TestCase):
'utm_source': self.request.user.username, 'utm_source': self.request.user.username,
'utm_medium': self.request.user.referral_tracking_id, 'utm_medium': self.request.user.referral_tracking_id,
}) })
) ),
} }
self.assertDictEqual(serializer.data, expected) self.assertDictEqual(serializer.data, expected)
...@@ -384,6 +389,22 @@ class ImageSerializerTests(TestCase): ...@@ -384,6 +389,22 @@ class ImageSerializerTests(TestCase):
self.assertDictEqual(serializer.data, expected) self.assertDictEqual(serializer.data, expected)
class NestedProgramSerializerTests(TestCase):
def test_data(self):
program = ProgramFactory()
serializer = NestedProgramSerializer(program)
expected = {
'uuid': str(program.uuid),
'marketing_slug': program.marketing_slug,
'marketing_url': program.marketing_url, # pylint: disable=no-member
'type': program.type.name,
'title': program.title,
}
self.assertDictEqual(serializer.data, expected)
class VideoSerializerTests(TestCase): class VideoSerializerTests(TestCase):
def test_data(self): def test_data(self):
video = VideoFactory() video = VideoFactory()
......
...@@ -7,7 +7,8 @@ from django.conf import settings ...@@ -7,7 +7,8 @@ from django.conf import settings
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from course_discovery.apps.api.serializers import ( from course_discovery.apps.api.serializers import (
CatalogSerializer, CourseSerializer, CourseSerializerExcludingClosedRuns, FlattenedCourseRunWithCourseSerializer CatalogSerializer, CourseWithProgramsSerializer, CourseSerializerExcludingClosedRuns,
FlattenedCourseRunWithCourseSerializer
) )
...@@ -27,7 +28,7 @@ class SerializationMixin(object): ...@@ -27,7 +28,7 @@ class SerializationMixin(object):
return self._serialize_object(CatalogSerializer, catalog, many, format) return self._serialize_object(CatalogSerializer, catalog, many, format)
def serialize_course(self, course, many=False, format=None): def serialize_course(self, course, many=False, format=None):
return self._serialize_object(CourseSerializer, course, many, format) return self._serialize_object(CourseWithProgramsSerializer, course, many, format)
def serialize_catalog_course(self, course, many=False, format=None): def serialize_catalog_course(self, course, many=False, format=None):
return self._serialize_object(CourseSerializerExcludingClosedRuns, course, many, format) return self._serialize_object(CourseSerializerExcludingClosedRuns, course, many, format)
......
...@@ -6,7 +6,7 @@ from django.db.models.functions import Lower ...@@ -6,7 +6,7 @@ from django.db.models.functions import Lower
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from rest_framework.test import APITestCase, APIRequestFactory from rest_framework.test import APITestCase, APIRequestFactory
from course_discovery.apps.api.serializers import CourseRunSerializer from course_discovery.apps.api.serializers import CourseRunWithProgramsSerializer
from course_discovery.apps.core.tests.factories import UserFactory from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.core.tests.mixins import ElasticsearchTestMixin from course_discovery.apps.core.tests.mixins import ElasticsearchTestMixin
from course_discovery.apps.course_metadata.models import CourseRun from course_discovery.apps.course_metadata.models import CourseRun
...@@ -27,7 +27,7 @@ class CourseRunViewSetTests(ElasticsearchTestMixin, APITestCase): ...@@ -27,7 +27,7 @@ class CourseRunViewSetTests(ElasticsearchTestMixin, APITestCase):
self.request.user = self.user self.request.user = self.user
def serialize_course_run(self, course_run, **kwargs): def serialize_course_run(self, course_run, **kwargs):
return CourseRunSerializer(course_run, context={'request': self.request}, **kwargs).data return CourseRunWithProgramsSerializer(course_run, context={'request': self.request}, **kwargs).data
def test_get(self): def test_get(self):
""" Verify the endpoint returns the details for a single course. """ """ Verify the endpoint returns the details for a single course. """
......
...@@ -181,7 +181,7 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet): ...@@ -181,7 +181,7 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet):
lookup_value_regex = COURSE_ID_REGEX lookup_value_regex = COURSE_ID_REGEX
queryset = Course.objects.all() queryset = Course.objects.all()
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
serializer_class = serializers.CourseSerializer serializer_class = serializers.CourseWithProgramsSerializer
def get_queryset(self): def get_queryset(self):
q = self.request.query_params.get('q', None) q = self.request.query_params.get('q', None)
...@@ -225,7 +225,7 @@ class CourseRunViewSet(viewsets.ReadOnlyModelViewSet): ...@@ -225,7 +225,7 @@ class CourseRunViewSet(viewsets.ReadOnlyModelViewSet):
lookup_value_regex = COURSE_RUN_ID_REGEX lookup_value_regex = COURSE_RUN_ID_REGEX
queryset = CourseRun.objects.all().order_by(Lower('key')) queryset = CourseRun.objects.all().order_by(Lower('key'))
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
serializer_class = serializers.CourseRunSerializer serializer_class = serializers.CourseRunWithProgramsSerializer
def _get_partner(self): def _get_partner(self):
""" Return the partner for the code passed in or the default partner """ """ Return the partner for the code passed in or the default partner """
......
...@@ -389,6 +389,10 @@ class CourseRun(TimeStampedModel): ...@@ -389,6 +389,10 @@ class CourseRun(TimeStampedModel):
return self.course.prerequisites return self.course.prerequisites
@property @property
def programs(self):
return self.course.programs
@property
def seat_types(self): def seat_types(self):
return list(self.seats.values_list('type', flat=True)) return list(self.seats.values_list('type', flat=True))
...@@ -576,7 +580,7 @@ class Program(TimeStampedModel): ...@@ -576,7 +580,7 @@ class Program(TimeStampedModel):
) )
marketing_slug = models.CharField( marketing_slug = models.CharField(
help_text=_('Slug used to generate links to the marketing site'), blank=True, max_length=255, db_index=True) help_text=_('Slug used to generate links to the marketing site'), blank=True, max_length=255, db_index=True)
courses = models.ManyToManyField(Course) courses = models.ManyToManyField(Course, related_name='programs')
# NOTE (CCB): Editors of this field should validate the values to ensure only CourseRuns associated # NOTE (CCB): Editors of this field should validate the values to ensure only CourseRuns associated
# with related Courses are stored. # with related Courses are stored.
excluded_course_runs = models.ManyToManyField(CourseRun, blank=True) excluded_course_runs = models.ManyToManyField(CourseRun, blank=True)
...@@ -623,7 +627,7 @@ class Program(TimeStampedModel): ...@@ -623,7 +627,7 @@ class Program(TimeStampedModel):
@property @property
def course_runs(self): def course_runs(self):
excluded_course_run_ids = [course_run.id for course_run in self.excluded_course_runs.all()] excluded_course_run_ids = [course_run.id for course_run in self.excluded_course_runs.all()]
return CourseRun.objects.filter(course__program=self).exclude(id__in=excluded_course_run_ids) return CourseRun.objects.filter(course__programs=self).exclude(id__in=excluded_course_run_ids)
@property @property
def languages(self): def languages(self):
......
...@@ -309,7 +309,13 @@ class ProgramTests(TestCase): ...@@ -309,7 +309,13 @@ class ProgramTests(TestCase):
self.assertIsNone(self.program.marketing_url) self.assertIsNone(self.program.marketing_url)
def test_course_runs(self): def test_course_runs(self):
""" Verify the property returns the set of associated CourseRuns minus those that are explicitly excluded. """ """
Verify that we only fetch course runs for the program, and not other course runs for other programs and that the
property returns the set of associated CourseRuns minus those that are explicitly excluded.
"""
course_run = factories.CourseRunFactory()
factories.ProgramFactory(courses=[course_run.course])
# Verify that course run is not returned in set
self.assertEqual(set(self.program.course_runs), set(self.course_runs)) self.assertEqual(set(self.program.course_runs), set(self.course_runs))
def test_languages(self): def test_languages(self):
......
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