Commit 11469da3 by Bill DeRusha

Add course run endpoint

parent 7af7880b
......@@ -3,7 +3,7 @@ from rest_framework import serializers
from course_discovery.apps.catalogs.models import Catalog
from course_discovery.apps.course_metadata.models import(
Course, Image, Organization, Prerequisite, Subject, Video
Course, CourseRun, Image, Organization, Person, Prerequisite, Seat, Subject, Video
)
......@@ -50,11 +50,34 @@ class VideoSerializer(MediaSerializer):
fields = ('src', 'description', 'image', )
class SeatSerializer(serializers.ModelSerializer):
type = serializers.ChoiceField(
choices=[name for name, __ in Seat.SEAT_TYPE_CHOICES]
)
price = serializers.DecimalField(
decimal_places=Seat.PRICE_FIELD_CONFIG['decimal_places'],
max_digits=Seat.PRICE_FIELD_CONFIG['max_digits']
)
currency = serializers.SlugRelatedField(read_only=True, slug_field='code')
upgrade_deadline = serializers.DateTimeField()
credit_provider = serializers.CharField()
credit_hours = serializers.IntegerField()
class Meta(object):
model = Seat
fields = ('type', 'price', 'currency', 'upgrade_deadline', 'credit_provider', 'credit_hours', )
class PersonSerializer(serializers.ModelSerializer):
profile_image = ImageSerializer()
class Meta(object):
model = Person
fields = ('name', 'title', 'bio', 'profile_image',)
class OrganizationSerializer(serializers.ModelSerializer):
name = serializers.CharField()
logo_image = ImageSerializer()
description = serializers.CharField()
homepage_url = serializers.CharField()
class Meta(object):
model = Organization
......@@ -86,6 +109,25 @@ class CourseSerializer(TimestampModelSerializer):
)
class CourseRunSerializer(TimestampModelSerializer):
content_language = serializers.SlugRelatedField(read_only=True, slug_field='code', source='language')
transcript_languages = serializers.SlugRelatedField(many=True, read_only=True, slug_field='code')
image = ImageSerializer()
video = VideoSerializer()
seats = SeatSerializer(many=True)
instructors = PersonSerializer(many=True)
staff = PersonSerializer(many=True)
class Meta(object):
model = CourseRun
fields = (
'key', 'title', 'short_description', 'full_description', 'start', 'end',
'enrollment_start', 'enrollment_end', 'announcement', 'image', 'video', 'seats',
'content_language', 'transcript_languages', 'instructors', 'staff',
'pacing_type', 'min_effort', 'max_effort', 'modified',
)
class ContainedCoursesSerializer(serializers.Serializer): # pylint: disable=abstract-method
courses = serializers.DictField(
child=serializers.BooleanField(),
......
......@@ -4,15 +4,21 @@ import ddt
from django.test import TestCase
from course_discovery.apps.api.serializers import(
CatalogSerializer, CourseSerializer, ContainedCoursesSerializer, ImageSerializer,
SubjectSerializer, PrerequisiteSerializer, VideoSerializer, OrganizationSerializer
CatalogSerializer, CourseSerializer, CourseRunSerializer, ContainedCoursesSerializer, ImageSerializer,
SubjectSerializer, PrerequisiteSerializer, VideoSerializer, OrganizationSerializer, SeatSerializer,
PersonSerializer,
)
from course_discovery.apps.catalogs.tests.factories import CatalogFactory
from course_discovery.apps.course_metadata.tests.factories import (
CourseFactory, SubjectFactory, PrerequisiteFactory, ImageFactory, VideoFactory, OrganizationFactory
CourseFactory, CourseRunFactory, SubjectFactory, PrerequisiteFactory,
ImageFactory, VideoFactory, OrganizationFactory, PersonFactory, SeatFactory
)
def json_date_format(datetime_obj):
return datetime.strftime(datetime_obj, "%Y-%m-%dT%H:%M:%S.%fZ")
class CatalogSerializerTests(TestCase):
def test_data(self):
catalog = CatalogFactory(query='*:*') # We intentionally use a query for all Courses.
......@@ -51,7 +57,40 @@ class CourseSerializerTests(TestCase):
'video': VideoSerializer(video).data,
'owners': [],
'sponsors': [],
'modified': datetime.strftime(course.modified, "%Y-%m-%dT%H:%M:%S.%fZ") # pylint: disable=no-member
'modified': json_date_format(course.modified) # pylint: disable=no-member
}
self.assertDictEqual(serializer.data, expected)
class CourseRunSerializerTests(TestCase):
def test_data(self):
course_run = CourseRunFactory()
image = course_run.image
video = course_run.video
serializer = CourseRunSerializer(course_run)
expected = {
'key': course_run.key,
'title': course_run.title, # pylint: disable=no-member
'short_description': course_run.short_description, # pylint: disable=no-member
'full_description': course_run.full_description, # pylint: disable=no-member
'start': json_date_format(course_run.start),
'end': json_date_format(course_run.end),
'enrollment_start': json_date_format(course_run.enrollment_start),
'enrollment_end': json_date_format(course_run.enrollment_end),
'announcement': json_date_format(course_run.announcement),
'image': ImageSerializer(image).data,
'video': VideoSerializer(video).data,
'pacing_type': course_run.pacing_type,
'content_language': course_run.language.code,
'transcript_languages': [],
'min_effort': course_run.min_effort,
'max_effort': course_run.max_effort,
'instructors': [],
'staff': [],
'seats': [],
'modified': json_date_format(course_run.modified) # pylint: disable=no-member
}
self.assertDictEqual(serializer.data, expected)
......@@ -70,7 +109,7 @@ class ContainedCoursesSerializerTests(TestCase):
@ddt.ddt
class LinkObjectSerializerTests(TestCase):
class NamedModelSerializerTests(TestCase):
@ddt.data(
(SubjectFactory, SubjectSerializer),
(PrerequisiteFactory, PrerequisiteSerializer),
......@@ -136,3 +175,37 @@ class OrganizationSerializerTests(TestCase):
}
self.assertDictEqual(serializer.data, expected)
class SeatSerializerTests(TestCase):
def test_data(self):
course_run = CourseRunFactory()
seat = SeatFactory(course_run=course_run)
serializer = SeatSerializer(seat)
expected = {
'type': seat.type,
'price': str(seat.price),
'currency': seat.currency.code,
'upgrade_deadline': json_date_format(seat.upgrade_deadline),
'credit_provider': seat.credit_provider, # pylint: disable=no-member
'credit_hours': seat.credit_hours # pylint: disable=no-member
}
self.assertDictEqual(serializer.data, expected)
class PersonSerializerTests(TestCase):
def test_data(self):
person = PersonFactory()
image = person.profile_image
serializer = PersonSerializer(person)
expected = {
'name': person.name,
'title': person.title,
'bio': person.bio,
'profile_image': ImageSerializer(image).data
}
self.assertDictEqual(serializer.data, expected)
# pylint: disable=no-member
from django.db.models.functions import Lower
from rest_framework.reverse import reverse
from rest_framework.test import APITestCase
from course_discovery.apps.api.serializers import CourseRunSerializer
from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory
from course_discovery.apps.course_metadata.models import CourseRun
class CourseRunViewSetTests(APITestCase):
def setUp(self):
super(CourseRunViewSetTests, self).setUp()
self.user = UserFactory(is_staff=True, is_superuser=True)
self.client.force_authenticate(self.user)
self.course_run = CourseRunFactory()
def test_get(self):
""" Verify the endpoint returns the details for a single course. """
url = reverse('api:v1:course_run-detail', kwargs={'key': self.course_run.key})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, CourseRunSerializer(self.course_run).data)
def test_list(self):
""" Verify the endpoint returns a list of all catalogs. """
url = reverse('api:v1:course_run-list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertListEqual(
response.data['results'],
CourseRunSerializer(CourseRun.objects.all().order_by(Lower('key')), many=True).data
)
from django.db.models.functions import Lower
from rest_framework.reverse import reverse
from rest_framework.test import APITestCase
......@@ -28,4 +29,7 @@ class CourseViewSetTests(SerializationMixin, APITestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertListEqual(response.data['results'], self.serialize_course(Course.objects.all(), many=True))
self.assertListEqual(
response.data['results'],
self.serialize_course(Course.objects.all().order_by(Lower('key')), many=True)
)
......@@ -8,5 +8,6 @@ urlpatterns = []
router = routers.SimpleRouter()
router.register(r'catalogs', views.CatalogViewSet)
router.register(r'courses', views.CourseViewSet, base_name='course')
router.register(r'course_runs', views.CourseRunViewSet, base_name='course_run')
urlpatterns += router.urls
......@@ -8,10 +8,12 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from course_discovery.apps.api.filters import PermissionsFilter
from course_discovery.apps.api.serializers import CatalogSerializer, CourseSerializer, ContainedCoursesSerializer
from course_discovery.apps.api.serializers import(
CatalogSerializer, CourseSerializer, CourseRunSerializer, ContainedCoursesSerializer
)
from course_discovery.apps.catalogs.models import Catalog
from course_discovery.apps.course_metadata.constants import COURSE_ID_REGEX
from course_discovery.apps.course_metadata.models import Course
from course_discovery.apps.course_metadata.constants import COURSE_ID_REGEX, COURSE_RUN_ID_REGEX
from course_discovery.apps.course_metadata.models import Course, CourseRun
logger = logging.getLogger(__name__)
......@@ -109,3 +111,21 @@ class CourseViewSet(viewsets.ReadOnlyModelViewSet):
def retrieve(self, request, *args, **kwargs):
""" Retrieve details for a course. """
return super(CourseViewSet, self).retrieve(request, *args, **kwargs)
class CourseRunViewSet(viewsets.ReadOnlyModelViewSet):
""" CourseRun resource. """
lookup_field = 'key'
lookup_value_regex = COURSE_RUN_ID_REGEX
queryset = CourseRun.objects.all().order_by(Lower('key'))
permission_classes = (IsAuthenticated,)
serializer_class = CourseRunSerializer
# The boilerplate methods are required to be recognized by swagger
def list(self, request, *args, **kwargs):
""" List all course runs. """
return super(CourseRunViewSet, self).list(request, *args, **kwargs)
def retrieve(self, request, *args, **kwargs):
""" Retrieve details for a course run. """
return super(CourseRunViewSet, self).retrieve(request, *args, **kwargs)
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course_metadata', '0002_auto_20160404_1626'),
]
operations = [
migrations.AlterField(
model_name='courserun',
name='course',
field=models.ForeignKey(related_name='course_runs', to='course_metadata.Course'),
),
migrations.AlterField(
model_name='historicalseat',
name='credit_hours',
field=models.IntegerField(null=True, blank=True),
),
migrations.AlterField(
model_name='historicalseat',
name='credit_provider',
field=models.CharField(max_length=255, null=True, blank=True),
),
migrations.AlterField(
model_name='historicalseat',
name='price',
field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10),
),
migrations.AlterField(
model_name='historicalseat',
name='upgrade_deadline',
field=models.DateTimeField(null=True, blank=True),
),
migrations.AlterField(
model_name='seat',
name='credit_hours',
field=models.IntegerField(null=True, blank=True),
),
migrations.AlterField(
model_name='seat',
name='credit_provider',
field=models.CharField(max_length=255, null=True, blank=True),
),
migrations.AlterField(
model_name='seat',
name='price',
field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10),
),
migrations.AlterField(
model_name='seat',
name='upgrade_deadline',
field=models.DateTimeField(null=True, blank=True),
),
]
......@@ -153,7 +153,7 @@ class CourseRun(TimeStampedModel):
(INSTRUCTOR_PACED, _('Instructor-paced')),
)
course = models.ForeignKey(Course)
course = models.ForeignKey(Course, related_name='course_runs')
key = models.CharField(max_length=255, unique=True)
title_override = models.CharField(
max_length=255, default=None, null=True, blank=True,
......@@ -240,13 +240,20 @@ class Seat(TimeStampedModel):
(PROFESSIONAL, _('Professional')),
(CREDIT, _('Credit')),
)
PRICE_FIELD_CONFIG = {
'decimal_places': 2,
'max_digits': 10,
'null': False,
'default': 0.00,
}
course_run = models.ForeignKey(CourseRun, related_name='seats')
type = models.CharField(max_length=63, choices=SEAT_TYPE_CHOICES)
price = models.DecimalField(decimal_places=2, max_digits=10)
price = models.DecimalField(**PRICE_FIELD_CONFIG)
currency = models.ForeignKey(Currency)
upgrade_deadline = models.DateTimeField()
credit_provider = models.CharField(max_length=255)
credit_hours = models.IntegerField()
upgrade_deadline = models.DateTimeField(null=True, blank=True)
credit_provider = models.CharField(max_length=255, null=True, blank=True)
credit_hours = models.IntegerField(null=True, blank=True)
history = HistoricalRecords()
......
from datetime import datetime
import factory
from factory.fuzzy import BaseFuzzyAttribute, FuzzyText, FuzzyChoice
from factory.fuzzy import(
BaseFuzzyAttribute, FuzzyText, FuzzyChoice, FuzzyDateTime, FuzzyInteger, FuzzyDecimal
)
from pytz import UTC
from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.core.models import Currency
from course_discovery.apps.course_metadata.models import(
Course, CourseRun, Organization, Person, Image, Video, Subject, Prerequisite, LevelType
Course, CourseRun, Organization, Person, Image, Video, Subject, Seat, Prerequisite, LevelType
)
......@@ -64,6 +71,16 @@ class PrerequisiteFactory(AbstractNamedModelFactory):
model = Prerequisite
class SeatFactory(factory.DjangoModelFactory):
type = FuzzyChoice([name for name, __ in Seat.SEAT_TYPE_CHOICES])
price = FuzzyDecimal(0.0, 650.0)
currency = factory.Iterator(Currency.objects.all())
upgrade_deadline = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC))
class Meta:
model = Seat
class CourseFactory(factory.DjangoModelFactory):
key = FuzzyText(prefix='course-id/')
title = FuzzyText(prefix="Test çօմɾʂҽ ")
......@@ -83,6 +100,17 @@ class CourseRunFactory(factory.DjangoModelFactory):
title_override = None
short_description_override = None
full_description_override = None
language = factory.Iterator(LanguageTag.objects.all())
start = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC))
end = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC)).end_dt
enrollment_start = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC))
enrollment_end = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC)).end_dt
announcement = FuzzyDateTime(datetime(2014, 1, 1, tzinfo=UTC))
image = factory.SubFactory(ImageFactory)
video = factory.SubFactory(VideoFactory)
min_effort = FuzzyInteger(1, 10)
max_effort = FuzzyInteger(10, 20)
pacing_type = FuzzyChoice([name for name, __ in CourseRun.PACING_CHOICES])
class Meta:
model = CourseRun
......@@ -104,6 +132,7 @@ class PersonFactory(factory.DjangoModelFactory):
name = FuzzyText()
title = FuzzyText()
bio = FuzzyText()
profile_image = factory.SubFactory(ImageFactory)
class Meta:
model = Person
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