Commit 23f57d59 by Mike Dikan Committed by mikedikan

Add subject list/details endpoints

ECOM-7905

Adding endpoints to get individual subjects and a full subject listing.  The subjects will be used on the marketing site to render the subject landing pages and eventually the all-subjects page.
parent f68f553d
...@@ -13,7 +13,7 @@ from rest_framework.exceptions import NotFound, PermissionDenied ...@@ -13,7 +13,7 @@ from rest_framework.exceptions import NotFound, PermissionDenied
from course_discovery.apps.api.utils import cast2int from course_discovery.apps.api.utils import cast2int
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 Course, CourseRun, Organization, Person, Program from course_discovery.apps.course_metadata.models import Course, CourseRun, Organization, Person, Program, Subject
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
User = get_user_model() User = get_user_model()
...@@ -178,3 +178,10 @@ class PersonFilter(filters.FilterSet): ...@@ -178,3 +178,10 @@ class PersonFilter(filters.FilterSet):
class Meta: class Meta:
model = Person model = Person
fields = ('slug',) fields = ('slug',)
class SubjectFilter(filters.FilterSet):
class Meta:
model = Subject
fields = ('slug', )
...@@ -164,9 +164,13 @@ class FAQSerializer(serializers.ModelSerializer): ...@@ -164,9 +164,13 @@ class FAQSerializer(serializers.ModelSerializer):
class SubjectSerializer(serializers.ModelSerializer): class SubjectSerializer(serializers.ModelSerializer):
"""Serializer for the ``Subject`` model.""" """Serializer for the ``Subject`` model."""
@classmethod
def prefetch_queryset(cls):
return Subject.objects.filter()
class Meta(object): class Meta(object):
model = Subject model = Subject
fields = ('name', 'subtitle', 'description', 'banner_image_url', 'card_image_url', 'slug',) fields = ('name', 'subtitle', 'description', 'banner_image_url', 'card_image_url', 'slug', 'uuid')
class PrerequisiteSerializer(NamedModelSerializer): class PrerequisiteSerializer(NamedModelSerializer):
......
...@@ -949,6 +949,7 @@ class SubjectSerializerTests(TestCase): ...@@ -949,6 +949,7 @@ class SubjectSerializerTests(TestCase):
'card_image_url': subject.card_image_url, 'card_image_url': subject.card_image_url,
'subtitle': subject.subtitle, 'subtitle': subject.subtitle,
'slug': subject.slug, 'slug': subject.slug,
'uuid': str(subject.uuid),
} }
self.assertDictEqual(serializer.data, expected) self.assertDictEqual(serializer.data, expected)
......
...@@ -10,7 +10,7 @@ from rest_framework.test import APIRequestFactory ...@@ -10,7 +10,7 @@ from rest_framework.test import APIRequestFactory
from course_discovery.apps.api.serializers import ( from course_discovery.apps.api.serializers import (
CatalogCourseSerializer, CatalogSerializer, CourseRunWithProgramsSerializer, CatalogCourseSerializer, CatalogSerializer, CourseRunWithProgramsSerializer,
CourseWithProgramsSerializer, FlattenedCourseRunWithCourseSerializer, MinimalProgramSerializer, CourseWithProgramsSerializer, FlattenedCourseRunWithCourseSerializer, MinimalProgramSerializer,
OrganizationSerializer, PersonSerializer, ProgramSerializer, ProgramTypeSerializer OrganizationSerializer, PersonSerializer, ProgramSerializer, ProgramTypeSerializer, SubjectSerializer
) )
from course_discovery.apps.api.tests.mixins import SiteMixin from course_discovery.apps.api.tests.mixins import SiteMixin
...@@ -67,6 +67,9 @@ class SerializationMixin(object): ...@@ -67,6 +67,9 @@ class SerializationMixin(object):
def serialize_organization(self, organization, many=False, format=None, extra_context=None): def serialize_organization(self, organization, many=False, format=None, extra_context=None):
return self._serialize_object(OrganizationSerializer, organization, many, format, extra_context) return self._serialize_object(OrganizationSerializer, organization, many, format, extra_context)
def serialize_subject(self, subject, many=False, format=None, extra_context=None):
return self._serialize_object(SubjectSerializer, subject, many, format, extra_context)
class OAuth2Mixin(object): class OAuth2Mixin(object):
def generate_oauth2_token_header(self, user): def generate_oauth2_token_header(self, user):
......
from django.urls import reverse
from course_discovery.apps.api.v1.tests.test_views.mixins import APITestCase, SerializationMixin
from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory
from course_discovery.apps.course_metadata.models import Subject
from course_discovery.apps.course_metadata.tests.factories import SubjectFactory
class SubjectViewSetTests(SerializationMixin, APITestCase):
list_path = reverse('api:v1:subject-list')
def setUp(self):
super(SubjectViewSetTests, self).setUp()
self.user = UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=self.user.username, password=USER_PASSWORD)
def test_authentication(self):
""" Verify the endpoint requires the user to be authenticated. """
response = self.client.get(self.list_path)
assert response.status_code == 200
self.client.logout()
response = self.client.get(self.list_path)
assert response.status_code == 403
def test_list(self):
""" Verify the endpoint returns a list of all subjects. """
SubjectFactory.create_batch(8)
expected = Subject.objects.all()
with self.assertNumQueries(5):
response = self.client.get(self.list_path)
assert response.status_code == 200
assert response.data['results'] == self.serialize_subject(expected, many=True)
def test_retrieve(self):
""" The request should return details for a single subject. """
subject = SubjectFactory()
url = reverse('api:v1:subject-detail', kwargs={'uuid': subject.uuid})
with self.assertNumQueries(4):
response = self.client.get(url)
assert response.status_code == 200
assert response.data == self.serialize_subject(subject)
...@@ -11,6 +11,7 @@ from course_discovery.apps.api.v1.views.organizations import OrganizationViewSet ...@@ -11,6 +11,7 @@ from course_discovery.apps.api.v1.views.organizations import OrganizationViewSet
from course_discovery.apps.api.v1.views.people import PersonViewSet from course_discovery.apps.api.v1.views.people import PersonViewSet
from course_discovery.apps.api.v1.views.program_types import ProgramTypeViewSet from course_discovery.apps.api.v1.views.program_types import ProgramTypeViewSet
from course_discovery.apps.api.v1.views.programs import ProgramViewSet from course_discovery.apps.api.v1.views.programs import ProgramViewSet
from course_discovery.apps.api.v1.views.subjects import SubjectViewSet
partners_router = routers.SimpleRouter() partners_router = routers.SimpleRouter()
partners_router.register(r'affiliate_window/catalogs', AffiliateWindowViewSet, base_name='affiliate_window') partners_router.register(r'affiliate_window/catalogs', AffiliateWindowViewSet, base_name='affiliate_window')
...@@ -26,6 +27,7 @@ router.register(r'courses', CourseViewSet, base_name='course') ...@@ -26,6 +27,7 @@ router.register(r'courses', CourseViewSet, base_name='course')
router.register(r'course_runs', CourseRunViewSet, base_name='course_run') router.register(r'course_runs', CourseRunViewSet, base_name='course_run')
router.register(r'organizations', OrganizationViewSet, base_name='organization') router.register(r'organizations', OrganizationViewSet, base_name='organization')
router.register(r'people', PersonViewSet, base_name='person') router.register(r'people', PersonViewSet, base_name='person')
router.register(r'subjects', SubjectViewSet, base_name='subject')
router.register(r'programs', ProgramViewSet, base_name='program') router.register(r'programs', ProgramViewSet, base_name='program')
router.register(r'program_types', ProgramTypeViewSet, base_name='program_type') router.register(r'program_types', ProgramTypeViewSet, base_name='program_type')
router.register(r'search/all', search_views.AggregateSearchViewSet, base_name='search-all') router.register(r'search/all', search_views.AggregateSearchViewSet, base_name='search-all')
......
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from course_discovery.apps.api import filters, serializers
from course_discovery.apps.api.pagination import ProxiedPagination
# pylint: disable=no-member
class SubjectViewSet(viewsets.ReadOnlyModelViewSet):
""" Subject resource. """
filter_backends = (DjangoFilterBackend,)
filter_class = filters.SubjectFilter
lookup_field = 'uuid'
lookup_value_regex = '[0-9a-f-]+'
permission_classes = (IsAuthenticated,)
serializer_class = serializers.SubjectSerializer
# Explicitly support PageNumberPagination and LimitOffsetPagination. Future
# versions of this API should only support the system default, PageNumberPagination.
pagination_class = ProxiedPagination
def get_queryset(self):
return serializers.SubjectSerializer.prefetch_queryset()
def list(self, request, *args, **kwargs):
""" Retrieve a list of all subjects. """
return super(SubjectViewSet, self).list(request, *args, **kwargs)
def retrieve(self, request, *args, **kwargs):
""" Retrieve details for an subject. """
return super(SubjectViewSet, self).retrieve(request, *args, **kwargs)
...@@ -52,6 +52,7 @@ class SubjectFactory(factory.DjangoModelFactory): ...@@ -52,6 +52,7 @@ class SubjectFactory(factory.DjangoModelFactory):
banner_image_url = FuzzyURL() banner_image_url = FuzzyURL()
card_image_url = FuzzyURL() card_image_url = FuzzyURL()
partner = factory.SubFactory(PartnerFactory) partner = factory.SubFactory(PartnerFactory)
uuid = factory.LazyFunction(uuid4)
class LevelTypeFactory(AbstractNamedModelFactory): class LevelTypeFactory(AbstractNamedModelFactory):
......
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