Commit 5e8c9f42 by Clinton Blackburn

Updated program_types endpoint

- Organized view in the same manner as all other views
- Using a ReadOnlyViewSet
- Returning applicable_seat_types in serialized response
- All ProgramType instances now have unique slugs

LEARNER-435
parent 0128283a
......@@ -868,16 +868,17 @@ class ProgramSerializer(MinimalProgramSerializer):
class ProgramTypeSerializer(serializers.ModelSerializer):
"""
Serializer for the Program Types
"""
""" Serializer for the Program Types. """
applicable_seat_types = serializers.SlugRelatedField(many=True, read_only=True, slug_field='slug')
logo_image = StdImageSerializerField()
@classmethod
def prefetch_queryset(cls, queryset):
return queryset.prefetch_related('applicable_seat_types')
class Meta:
model = ProgramType
fields = (
'name', 'logo_image',
)
fields = ('name', 'logo_image', 'applicable_seat_types', 'slug',)
class AffiliateWindowSerializer(serializers.ModelSerializer):
......
......@@ -30,7 +30,7 @@ from course_discovery.apps.course_metadata.models import Course, CourseRun, Prog
from course_discovery.apps.course_metadata.tests.factories import (
CorporateEndorsementFactory, CourseFactory, CourseRunFactory, EndorsementFactory, ExpectedLearningItemFactory,
ImageFactory, JobOutlookItemFactory, OrganizationFactory, PersonFactory, PositionFactory, PrerequisiteFactory,
ProgramFactory, ProgramTypeFactory, SeatFactory, SubjectFactory, VideoFactory
ProgramFactory, ProgramTypeFactory, SeatFactory, SeatTypeFactory, SubjectFactory, VideoFactory
)
from course_discovery.apps.ietf_language_tags.models import LanguageTag
......@@ -855,11 +855,14 @@ class ProgramTypeSerializerTests(TestCase):
return {
'name': program_type.name,
'logo_image': image_field.to_representation(program_type.logo_image),
'applicable_seat_types': [seat_type.slug for seat_type in program_type.applicable_seat_types.all()],
'slug': program_type.slug,
}
def test_data(self):
request = make_request()
program_type = ProgramTypeFactory()
applicable_seat_types = SeatTypeFactory.create_batch(3)
program_type = ProgramTypeFactory(applicable_seat_types=applicable_seat_types)
serializer = self.serializer_class(program_type, context={'request': request})
expected = self.get_expected_data(program_type, request)
self.assertDictEqual(serializer.data, expected)
......
from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase
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.course_metadata.models import ProgramType
from course_discovery.apps.course_metadata.tests.factories import ProgramTypeFactory
class ProgramTypeViewSetTests(SerializationMixin, APITestCase):
list_path = reverse('api:v1:program_type-list')
def setUp(self):
super(ProgramTypeViewSetTests, 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 program types. """
ProgramTypeFactory.create_batch(4)
expected = ProgramType.objects.all()
with self.assertNumQueries(5):
response = self.client.get(self.list_path)
assert response.status_code == 200
assert response.data['results'] == self.serialize_program_type(expected, many=True)
def test_retrieve(self):
""" The request should return details for a single program type. """
program_type = ProgramTypeFactory()
url = reverse('api:v1:program_type-detail', kwargs={'slug': program_type.slug})
with self.assertNumQueries(4):
response = self.client.get(url)
assert response.status_code == 200
assert response.data == self.serialize_program_type(program_type)
......@@ -11,10 +11,10 @@ from course_discovery.apps.api.v1.views.programs import ProgramViewSet
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.course_metadata.choices import ProgramStatus
from course_discovery.apps.course_metadata.models import Program, ProgramType
from course_discovery.apps.course_metadata.models import Program
from course_discovery.apps.course_metadata.tests.factories import (
CorporateEndorsementFactory, CourseFactory, CourseRunFactory, EndorsementFactory, ExpectedLearningItemFactory,
JobOutlookItemFactory, OrganizationFactory, PersonFactory, ProgramFactory, ProgramTypeFactory, VideoFactory
JobOutlookItemFactory, OrganizationFactory, PersonFactory, ProgramFactory, VideoFactory
)
......@@ -235,49 +235,3 @@ class ProgramViewSetTests(SerializationMixin, APITestCase):
def test_minimal_serializer_use(self):
""" Verify that the list view uses the minimal serializer. """
self.assertEqual(ProgramViewSet(action='list').get_serializer_class(), MinimalProgramSerializer)
class ProgramTypeViewSetTests(SerializationMixin, APITestCase):
list_path = reverse('api:v1:program_type-list')
def setUp(self):
super(ProgramTypeViewSetTests, 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)
self.assertEqual(response.status_code, 200)
self.client.logout()
response = self.client.get(self.list_path)
self.assertEqual(response.status_code, 403)
def assert_list_results(self, url, expected, expected_query_count, extra_context=None):
"""
Asserts the results serialized/returned at the URL matches those that are expected.
Args:
url (str): URL from which data should be retrieved
expected (list[ProgramType]): Expected program_types
Notes:
The API usually returns items in reverse order of creation (e.g. newest first). You may need to reverse
the values of `expected` if you encounter issues. This method will NOT do that reversal for you.
Returns:
None
"""
with self.assertNumQueries(expected_query_count):
response = self.client.get(url)
self.assertEqual(
response.data['results'],
self.serialize_program_type(expected, many=True, extra_context=extra_context)
)
def test_list(self):
""" Verify the endpoint returns a list of all program_types. """
ProgramTypeFactory.create_batch(3)
expected = ProgramType.objects.all()
self.assert_list_results(self.list_path, expected, 4)
......@@ -9,7 +9,8 @@ from course_discovery.apps.api.v1.views.course_runs import CourseRunViewSet
from course_discovery.apps.api.v1.views.courses import CourseViewSet
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.programs import ProgramTypeListViewSet, ProgramViewSet
from course_discovery.apps.api.v1.views.program_types import ProgramTypeViewSet
from course_discovery.apps.api.v1.views.programs import ProgramViewSet
partners_router = routers.SimpleRouter()
partners_router.register(r'affiliate_window/catalogs', AffiliateWindowViewSet, base_name='affiliate_window')
......@@ -26,7 +27,7 @@ router.register(r'course_runs', CourseRunViewSet, base_name='course_run')
router.register(r'organizations', OrganizationViewSet, base_name='organization')
router.register(r'people', PersonViewSet, base_name='person')
router.register(r'programs', ProgramViewSet, base_name='program')
router.register(r'program_types', ProgramTypeListViewSet, 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/courses', search_views.CourseSearchViewSet, base_name='search-courses')
router.register(r'search/course_runs', search_views.CourseRunSearchViewSet, base_name='search-course_runs')
......
from rest_framework import viewsets
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import IsAuthenticated
from course_discovery.apps.api import serializers
from course_discovery.apps.course_metadata.models import ProgramType
class ProgramTypeViewSet(viewsets.ReadOnlyModelViewSet):
""" ProgramType resource. """
lookup_field = 'slug'
pagination_class = PageNumberPagination
permission_classes = (IsAuthenticated,)
queryset = serializers.ProgramTypeSerializer.prefetch_queryset(ProgramType.objects.all())
serializer_class = serializers.ProgramTypeSerializer
from rest_framework import mixins, viewsets
from rest_framework import viewsets
from rest_framework.filters import DjangoFilterBackend
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
......@@ -7,7 +7,7 @@ from rest_framework_extensions.cache.mixins import CacheResponseMixin
from course_discovery.apps.api import filters, serializers
from course_discovery.apps.api.pagination import ProxiedPagination
from course_discovery.apps.api.v1.views import get_query_param
from course_discovery.apps.course_metadata.models import Program, ProgramType
from course_discovery.apps.course_metadata.models import Program
# pylint: disable=no-member
......@@ -95,15 +95,3 @@ class ProgramViewSet(CacheResponseMixin, viewsets.ReadOnlyModelViewSet):
return Response(uuids)
return super(ProgramViewSet, self).list(request, *args, **kwargs)
class ProgramTypeListViewSet(mixins.ListModelMixin,
viewsets.GenericViewSet):
""" ProgramType resource. """
serializer_class = serializers.ProgramTypeSerializer
permission_classes = (IsAuthenticated,)
queryset = ProgramType.objects.all()
# Explicitly support PageNumberPagination and LimitOffsetPagination. Future
# versions of this API should only support the system default, PageNumberPagination.
pagination_class = ProxiedPagination
......@@ -198,7 +198,7 @@ class ProgramAdmin(admin.ModelAdmin):
@admin.register(ProgramType)
class ProgramTypeAdmin(admin.ModelAdmin):
list_display = ('name',)
list_display = ('name', 'slug',)
@admin.register(SeatType)
......
# -*- coding: utf-8 -*-
# Generated by Django 1.9.13 on 2017-05-05 02:32
from __future__ import unicode_literals
import django_extensions.db.fields
from django.db import migrations
from django.db.models import Q
from django.template.defaultfilters import slugify
def update_slugs(apps, schema_editor):
ProgramType = apps.get_model('course_metadata', 'ProgramType')
for program_type in ProgramType.objects.filter(Q(slug='') | Q(slug__isnull=True)):
program_type.slug = slugify(program_type.name)
program_type.save()
class Migration(migrations.Migration):
dependencies = [
('course_metadata', '0053_person_email'),
]
operations = [
migrations.RunPython(update_slugs, reverse_code=migrations.RunPython.noop),
migrations.AlterField(
model_name='programtype',
name='slug',
field=django_extensions.db.fields.AutoSlugField(
blank=True, editable=False,
help_text='Leave this field blank to have the value generated automatically.',
populate_from='name', unique=True),
),
]
......@@ -714,7 +714,7 @@ class ProgramType(TimeStampedModel):
},
help_text=_('Please provide an image file with transparent background'),
)
slug = AutoSlugField(populate_from='name', editable=True, blank=True,
slug = AutoSlugField(populate_from='name', editable=True, unique=True,
help_text=_('Leave this field blank to have the value generated automatically.'))
def __str__(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