Commit a0ccf5e9 by Renzo Lucioni Committed by GitHub

Reduce data returned by programs list view (#366)

Re-introduces minimal serializers used to avoid expensive serialization of data not used by any clients.

ECOM-5791
parent acb7fdc9
from django.db import migrations
def create_switch(apps, schema_editor):
"""Create the reduced_program_data switch."""
Switch = apps.get_model('waffle', 'Switch')
Switch.objects.get_or_create(name='reduced_program_data', defaults={'active': False})
def delete_switch(apps, schema_editor):
"""Delete the reduced_program_data switch."""
Switch = apps.get_model('waffle', 'Switch')
Switch.objects.filter(name='reduced_program_data').delete()
class Migration(migrations.Migration):
dependencies = [
('waffle', '0001_initial'),
]
operations = [
migrations.RunPython(create_switch, reverse_code=delete_switch),
]
...@@ -2,11 +2,14 @@ import ddt ...@@ -2,11 +2,14 @@ import ddt
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase, APIRequestFactory from rest_framework.test import APITestCase, APIRequestFactory
from course_discovery.apps.api.serializers import MinimalProgramSerializer, ProgramSerializer
from course_discovery.apps.api.v1.views import ProgramViewSet
from course_discovery.apps.api.v1.tests.test_views.mixins import SerializationMixin from course_discovery.apps.api.v1.tests.test_views.mixins import SerializationMixin
from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory
from course_discovery.apps.core.tests.helpers import make_image_file from course_discovery.apps.core.tests.helpers import make_image_file
from course_discovery.apps.course_metadata.choices import ProgramStatus from course_discovery.apps.course_metadata.choices import ProgramStatus
from course_discovery.apps.course_metadata.models import Program from course_discovery.apps.course_metadata.models import Program
from course_discovery.apps.course_metadata.tests import toggle_switch
from course_discovery.apps.course_metadata.tests.factories import ( from course_discovery.apps.course_metadata.tests.factories import (
CourseFactory, CourseRunFactory, VideoFactory, OrganizationFactory, PersonFactory, ProgramFactory, CourseFactory, CourseRunFactory, VideoFactory, OrganizationFactory, PersonFactory, ProgramFactory,
CorporateEndorsementFactory, EndorsementFactory, JobOutlookItemFactory, ExpectedLearningItemFactory CorporateEndorsementFactory, EndorsementFactory, JobOutlookItemFactory, ExpectedLearningItemFactory
...@@ -64,20 +67,17 @@ class ProgramViewSetTests(SerializationMixin, APITestCase): ...@@ -64,20 +67,17 @@ class ProgramViewSetTests(SerializationMixin, APITestCase):
def test_retrieve(self): def test_retrieve(self):
""" Verify the endpoint returns the details for a single program. """ """ Verify the endpoint returns the details for a single program. """
program = self.create_program() program = self.create_program()
with self.assertNumQueries(89): with self.assertNumQueries(72):
self.assert_retrieve_success(program) self.assert_retrieve_success(program)
@ddt.data( @ddt.data(True, False)
(True), def test_retrieve_with_sorting_flag(self, order_courses_by_start_date):
(False),
)
def test_retrieve_with_sorting_flag(self, order_courses_by_start_date=True):
""" Verify the number of queries is the same with sorting flag set to true. """ """ Verify the number of queries is the same with sorting flag set to true. """
course_list = CourseFactory.create_batch(3) course_list = CourseFactory.create_batch(3)
for course in course_list: for course in course_list:
CourseRunFactory(course=course) CourseRunFactory(course=course)
program = ProgramFactory(courses=course_list, order_courses_by_start_date=order_courses_by_start_date) program = ProgramFactory(courses=course_list, order_courses_by_start_date=order_courses_by_start_date)
num_queries = 132 if order_courses_by_start_date else 114 num_queries = 82 if order_courses_by_start_date else 80
with self.assertNumQueries(num_queries): with self.assertNumQueries(num_queries):
self.assert_retrieve_success(program) self.assert_retrieve_success(program)
self.assertEqual(course_list, list(program.courses.all())) # pylint: disable=no-member self.assertEqual(course_list, list(program.courses.all())) # pylint: disable=no-member
...@@ -86,7 +86,7 @@ class ProgramViewSetTests(SerializationMixin, APITestCase): ...@@ -86,7 +86,7 @@ class ProgramViewSetTests(SerializationMixin, APITestCase):
""" Verify the endpoint returns data for a program even if the program's courses have no course runs. """ """ Verify the endpoint returns data for a program even if the program's courses have no course runs. """
course = CourseFactory() course = CourseFactory()
program = ProgramFactory(courses=[course]) program = ProgramFactory(courses=[course])
with self.assertNumQueries(55): with self.assertNumQueries(46):
self.assert_retrieve_success(program) self.assert_retrieve_success(program)
def assert_list_results(self, url, expected, expected_query_count, extra_context=None): def assert_list_results(self, url, expected, expected_query_count, extra_context=None):
...@@ -115,7 +115,7 @@ class ProgramViewSetTests(SerializationMixin, APITestCase): ...@@ -115,7 +115,7 @@ class ProgramViewSetTests(SerializationMixin, APITestCase):
""" Verify the endpoint returns a list of all programs. """ """ Verify the endpoint returns a list of all programs. """
expected = [self.create_program() for __ in range(3)] expected = [self.create_program() for __ in range(3)]
expected.reverse() expected.reverse()
self.assert_list_results(self.list_path, expected, 41) self.assert_list_results(self.list_path, expected, 36)
def test_filter_by_type(self): def test_filter_by_type(self):
""" Verify that the endpoint filters programs to those of a given type. """ """ Verify that the endpoint filters programs to those of a given type. """
...@@ -159,4 +159,17 @@ class ProgramViewSetTests(SerializationMixin, APITestCase): ...@@ -159,4 +159,17 @@ class ProgramViewSetTests(SerializationMixin, APITestCase):
""" Verify the endpoint returns marketing URLs without UTM parameters. """ """ Verify the endpoint returns marketing URLs without UTM parameters. """
url = self.list_path + '?exclude_utm=1' url = self.list_path + '?exclude_utm=1'
program = self.create_program() program = self.create_program()
self.assert_list_results(url, [program], 33, extra_context={'exclude_utm': 1}) self.assert_list_results(url, [program], 32, extra_context={'exclude_utm': 1})
@ddt.data(True, False)
def test_minimal_serializer_use(self, is_minimal):
""" Verify that the list view uses the minimal serializer. """
toggle_switch('reduced_program_data', active=is_minimal)
action_serializer_mapping = {
'list': MinimalProgramSerializer if is_minimal else ProgramSerializer,
'detail': ProgramSerializer,
}
for action, serializer in action_serializer_mapping.items():
self.assertEqual(ProgramViewSet(action=action).get_serializer_class(), serializer)
...@@ -22,6 +22,7 @@ from rest_framework.exceptions import PermissionDenied, ParseError ...@@ -22,6 +22,7 @@ from rest_framework.exceptions import PermissionDenied, ParseError
from rest_framework.filters import DjangoFilterBackend from rest_framework.filters import DjangoFilterBackend
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
import waffle
from course_discovery.apps.api import filters from course_discovery.apps.api import filters
from course_discovery.apps.api import serializers from course_discovery.apps.api import serializers
...@@ -415,14 +416,19 @@ class ProgramViewSet(viewsets.ReadOnlyModelViewSet): ...@@ -415,14 +416,19 @@ class ProgramViewSet(viewsets.ReadOnlyModelViewSet):
lookup_field = 'uuid' lookup_field = 'uuid'
lookup_value_regex = '[0-9a-f-]+' lookup_value_regex = '[0-9a-f-]+'
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
serializer_class = serializers.ProgramSerializer
filter_backends = (DjangoFilterBackend,) filter_backends = (DjangoFilterBackend,)
filter_class = filters.ProgramFilter filter_class = filters.ProgramFilter
def get_serializer_class(self):
if self.action == 'list' and waffle.switch_is_active('reduced_program_data'):
return serializers.MinimalProgramSerializer
return serializers.ProgramSerializer
def get_queryset(self): def get_queryset(self):
# This method prevents prefetches on the program queryset from "stacking," # This method prevents prefetches on the program queryset from "stacking,"
# which happens when the queryset is stored in a class property. # which happens when the queryset is stored in a class property.
return self.serializer_class.prefetch_queryset() return self.get_serializer_class().prefetch_queryset()
def get_serializer_context(self, *args, **kwargs): def get_serializer_context(self, *args, **kwargs):
context = super().get_serializer_context(*args, **kwargs) context = super().get_serializer_context(*args, **kwargs)
......
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