Commit 5107ad29 by Mike Dikan

Update to course API to include (condensed) program info

parent 125bf70c
......@@ -189,6 +189,19 @@ class CatalogSerializer(serializers.ModelSerializer):
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):
"""Serializer for the ``CourseRun`` model."""
course = serializers.SlugRelatedField(read_only=True, slug_field='key')
......@@ -218,6 +231,15 @@ class CourseRunSerializer(TimestampModelSerializer):
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):
"""Serializer used to represent course runs contained by a catalog."""
course_runs = serializers.DictField(
......@@ -244,13 +266,22 @@ class CourseSerializer(TimestampModelSerializer):
fields = (
'key', 'title', 'short_description', 'full_description', 'level_type', 'subjects', 'prerequisites',
'expected_learning_items', 'image', 'video', 'owners', 'sponsors', 'modified', 'course_runs',
'marketing_url'
'marketing_url',
)
def get_marketing_url(self, obj):
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):
"""A ``CourseSerializer`` which only includes active course runs, as determined by ``CourseQuerySet``."""
course_runs = CourseRunSerializer(many=True, source='active_course_runs')
......
......@@ -11,7 +11,8 @@ from course_discovery.apps.api.serializers import (
CatalogSerializer, CourseSerializer, CourseRunSerializer, ContainedCoursesSerializer, ImageSerializer,
SubjectSerializer, PrerequisiteSerializer, VideoSerializer, OrganizationSerializer, SeatSerializer,
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.core.models import User
......@@ -76,7 +77,7 @@ class CourseSerializerTests(TestCase):
request = make_request()
CourseRunFactory.create_batch(3, course=course)
serializer = CourseSerializer(course, context={'request': request})
serializer = CourseWithProgramsSerializer(course, context={'request': request})
expected = {
'key': course.key,
......@@ -99,7 +100,8 @@ class CourseSerializerTests(TestCase):
'utm_source': request.user.username,
'utm_medium': request.user.referral_tracking_id,
})
)
),
'programs': NestedProgramSerializer(course.programs, many=True, context={'request': request}).data,
}
self.assertDictEqual(serializer.data, expected)
......@@ -119,9 +121,11 @@ class CourseRunSerializerTests(TestCase):
def test_data(self):
request = make_request()
course_run = CourseRunFactory()
course = course_run.course
image = course_run.image
video = course_run.video
serializer = CourseRunSerializer(course_run, context={'request': request})
serializer = CourseRunWithProgramsSerializer(course_run, context={'request': request})
ProgramFactory(courses=[course])
expected = {
'course': course_run.course.key,
......@@ -154,6 +158,7 @@ class CourseRunSerializerTests(TestCase):
),
'level_type': course_run.level_type.name,
'availability': course_run.availability,
'programs': NestedProgramSerializer(course.programs, many=True, context={'request': request}).data,
}
self.assertDictEqual(serializer.data, expected)
......@@ -239,7 +244,7 @@ class ProgramCourseSerializerTests(TestCase):
'utm_source': self.request.user.username,
'utm_medium': self.request.user.referral_tracking_id,
})
)
),
}
self.assertDictEqual(serializer.data, expected)
......@@ -384,6 +389,22 @@ class ImageSerializerTests(TestCase):
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):
def test_data(self):
video = VideoFactory()
......
......@@ -7,7 +7,8 @@ from django.conf import settings
from rest_framework.test import APIRequestFactory
from course_discovery.apps.api.serializers import (
CatalogSerializer, CourseSerializer, CourseSerializerExcludingClosedRuns, FlattenedCourseRunWithCourseSerializer
CatalogSerializer, CourseWithProgramsSerializer, CourseSerializerExcludingClosedRuns,
FlattenedCourseRunWithCourseSerializer
)
......@@ -27,7 +28,7 @@ class SerializationMixin(object):
return self._serialize_object(CatalogSerializer, catalog, many, format)
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):
return self._serialize_object(CourseSerializerExcludingClosedRuns, course, many, format)
......
......@@ -6,7 +6,7 @@ from django.db.models.functions import Lower
from rest_framework.reverse import reverse
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.mixins import ElasticsearchTestMixin
from course_discovery.apps.course_metadata.models import CourseRun
......@@ -27,7 +27,7 @@ class CourseRunViewSetTests(ElasticsearchTestMixin, APITestCase):
self.request.user = self.user
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):
""" Verify the endpoint returns the details for a single course. """
......
......@@ -181,7 +181,7 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet):
lookup_value_regex = COURSE_ID_REGEX
queryset = Course.objects.all()
permission_classes = (IsAuthenticated,)
serializer_class = serializers.CourseSerializer
serializer_class = serializers.CourseWithProgramsSerializer
def get_queryset(self):
q = self.request.query_params.get('q', None)
......@@ -225,7 +225,7 @@ class CourseRunViewSet(viewsets.ReadOnlyModelViewSet):
lookup_value_regex = COURSE_RUN_ID_REGEX
queryset = CourseRun.objects.all().order_by(Lower('key'))
permission_classes = (IsAuthenticated,)
serializer_class = serializers.CourseRunSerializer
serializer_class = serializers.CourseRunWithProgramsSerializer
def _get_partner(self):
""" Return the partner for the code passed in or the default partner """
......
......@@ -389,6 +389,10 @@ class CourseRun(TimeStampedModel):
return self.course.prerequisites
@property
def programs(self):
return self.course.programs
@property
def seat_types(self):
return list(self.seats.values_list('type', flat=True))
......@@ -576,7 +580,7 @@ class Program(TimeStampedModel):
)
marketing_slug = models.CharField(
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
# with related Courses are stored.
excluded_course_runs = models.ManyToManyField(CourseRun, blank=True)
......@@ -623,7 +627,7 @@ class Program(TimeStampedModel):
@property
def course_runs(self):
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
def languages(self):
......
......@@ -309,7 +309,13 @@ class ProgramTests(TestCase):
self.assertIsNone(self.program.marketing_url)
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))
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