Commit d3000dd2 by Dave St.Germain

Expose course_id to the API

parent 01f3b9c1
...@@ -3,9 +3,10 @@ Admin file for django app edxval. ...@@ -3,9 +3,10 @@ Admin file for django app edxval.
""" """
from django.contrib import admin from django.contrib import admin
from .models import Video, Profile, EncodedVideo, Subtitle from .models import Video, Profile, EncodedVideo, Subtitle, CourseVideos
admin.site.register(Video) admin.site.register(Video)
admin.site.register(Profile) admin.site.register(Profile)
admin.site.register(EncodedVideo) admin.site.register(EncodedVideo)
admin.site.register(Subtitle) admin.site.register(Subtitle)
admin.site.register(CourseVideos)
...@@ -170,7 +170,7 @@ def get_video_info(edx_video_id, location=None): # pylint: disable=W0613 ...@@ -170,7 +170,7 @@ def get_video_info(edx_video_id, location=None): # pylint: disable=W0613
} }
""" """
try: try:
video = Video.objects.get(edx_video_id=edx_video_id) video = Video.objects.prefetch_related("encoded_videos", "courses").get(edx_video_id=edx_video_id)
result = VideoSerializer(video) result = VideoSerializer(video)
except Video.DoesNotExist: except Video.DoesNotExist:
error_message = u"Video not found for edx_video_id: {0}".format(edx_video_id) error_message = u"Video not found for edx_video_id: {0}".format(edx_video_id)
...@@ -180,3 +180,10 @@ def get_video_info(edx_video_id, location=None): # pylint: disable=W0613 ...@@ -180,3 +180,10 @@ def get_video_info(edx_video_id, location=None): # pylint: disable=W0613
logger.exception(error_message) logger.exception(error_message)
raise ValInternalError(error_message) raise ValInternalError(error_message)
return result.data # pylint: disable=E1101 return result.data # pylint: disable=E1101
def get_videos_for_course(course_id):
"""
Returns an iterator of videos for the given course id
"""
videos = Video.objects.filter(courses__course_id=course_id)
return (VideoSerializer(video).data for video in videos)
...@@ -79,6 +79,9 @@ class Video(models.Model): ...@@ -79,6 +79,9 @@ class Video(models.Model):
client_video_id = models.CharField(max_length=255, db_index=True) client_video_id = models.CharField(max_length=255, db_index=True)
duration = models.FloatField(validators=[MinValueValidator(0)]) duration = models.FloatField(validators=[MinValueValidator(0)])
def get_absolute_url(self):
return reverse('video-detail', args=[self.edx_video_id])
def __str__(self): def __str__(self):
return self.edx_video_id return self.edx_video_id
...@@ -91,7 +94,7 @@ class CourseVideos(models.Model): ...@@ -91,7 +94,7 @@ class CourseVideos(models.Model):
course_id's but each pair is unique together. course_id's but each pair is unique together.
""" """
course_id = models.CharField(max_length=255) course_id = models.CharField(max_length=255)
video = models.ForeignKey(Video) video = models.ForeignKey(Video, related_name='courses')
class Meta: # pylint: disable=C1001 class Meta: # pylint: disable=C1001
""" """
...@@ -99,6 +102,9 @@ class CourseVideos(models.Model): ...@@ -99,6 +102,9 @@ class CourseVideos(models.Model):
""" """
unique_together = ("course_id", "video") unique_together = ("course_id", "video")
def __str__(self):
return '%s for %s' % (self.video, self.course_id)
class EncodedVideo(models.Model): class EncodedVideo(models.Model):
""" """
......
...@@ -7,7 +7,7 @@ EncodedVideoSerializer which uses the profile_name as it's profile field. ...@@ -7,7 +7,7 @@ EncodedVideoSerializer which uses the profile_name as it's profile field.
from rest_framework import serializers from rest_framework import serializers
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from edxval.models import Profile, Video, EncodedVideo, Subtitle from edxval.models import Profile, Video, EncodedVideo, Subtitle, CourseVideos
class ProfileSerializer(serializers.ModelSerializer): class ProfileSerializer(serializers.ModelSerializer):
...@@ -84,6 +84,18 @@ class SubtitleSerializer(serializers.ModelSerializer): ...@@ -84,6 +84,18 @@ class SubtitleSerializer(serializers.ModelSerializer):
) )
class CourseSerializer(serializers.RelatedField):
"""
Field for CourseVideos
"""
def to_native(self, value):
return value.course_id
def from_native(self, data):
if data:
return CourseVideos(course_id=data)
class VideoSerializer(serializers.ModelSerializer): class VideoSerializer(serializers.ModelSerializer):
""" """
Serializer for Video object Serializer for Video object
...@@ -92,12 +104,20 @@ class VideoSerializer(serializers.ModelSerializer): ...@@ -92,12 +104,20 @@ class VideoSerializer(serializers.ModelSerializer):
""" """
encoded_videos = EncodedVideoSerializer(many=True, allow_add_remove=True) encoded_videos = EncodedVideoSerializer(many=True, allow_add_remove=True)
subtitles = SubtitleSerializer(many=True, allow_add_remove=True, required=False) subtitles = SubtitleSerializer(many=True, allow_add_remove=True, required=False)
courses = CourseSerializer(many=True, read_only=False)
url = serializers.SerializerMethodField('get_url')
class Meta: # pylint: disable=C0111 class Meta: # pylint: disable=C0111
model = Video model = Video
lookup_field = "edx_video_id" lookup_field = "edx_video_id"
exclude = ('id',) exclude = ('id',)
def get_url(self, obj):
"""
Return relative url for the object
"""
return obj.get_absolute_url()
def restore_fields(self, data, files): def restore_fields(self, data, files):
""" """
Overridden function used to check against duplicate profile names. Overridden function used to check against duplicate profile names.
...@@ -111,6 +131,7 @@ class VideoSerializer(serializers.ModelSerializer): ...@@ -111,6 +131,7 @@ class VideoSerializer(serializers.ModelSerializer):
if data is not None and not isinstance(data, dict): if data is not None and not isinstance(data, dict):
self._errors['non_field_errors'] = ['Invalid data'] self._errors['non_field_errors'] = ['Invalid data']
return None return None
try: try:
profiles = [ev["profile"] for ev in data.get("encoded_videos", [])] profiles = [ev["profile"] for ev in data.get("encoded_videos", [])]
if len(profiles) != len(set(profiles)): if len(profiles) != len(set(profiles)):
......
...@@ -12,7 +12,7 @@ from django.core.exceptions import ValidationError ...@@ -12,7 +12,7 @@ from django.core.exceptions import ValidationError
from rest_framework import status from rest_framework import status
from ddt import ddt, data from ddt import ddt, data
from edxval.models import Profile, Video, EncodedVideo from edxval.models import Profile, Video, EncodedVideo, CourseVideos
from edxval import api as api from edxval import api as api
from edxval.api import ValCannotCreateError from edxval.api import ValCannotCreateError
from edxval.serializers import VideoSerializer from edxval.serializers import VideoSerializer
...@@ -123,7 +123,7 @@ class GetVideoInfoTest(TestCase): ...@@ -123,7 +123,7 @@ class GetVideoInfoTest(TestCase):
""" """
Profile.objects.create(**constants.PROFILE_DICT_MOBILE) Profile.objects.create(**constants.PROFILE_DICT_MOBILE)
Profile.objects.create(**constants.PROFILE_DICT_DESKTOP) Profile.objects.create(**constants.PROFILE_DICT_DESKTOP)
Video.objects.create(**constants.VIDEO_DICT_FISH) video = Video.objects.create(**constants.VIDEO_DICT_FISH)
EncodedVideo.objects.create( EncodedVideo.objects.create(
video=Video.objects.get( video=Video.objects.get(
edx_video_id=constants.VIDEO_DICT_FISH.get("edx_video_id") edx_video_id=constants.VIDEO_DICT_FISH.get("edx_video_id")
...@@ -138,6 +138,8 @@ class GetVideoInfoTest(TestCase): ...@@ -138,6 +138,8 @@ class GetVideoInfoTest(TestCase):
profile=Profile.objects.get(profile_name="desktop"), profile=Profile.objects.get(profile_name="desktop"),
**constants.ENCODED_VIDEO_DICT_DESKTOP **constants.ENCODED_VIDEO_DICT_DESKTOP
) )
self.course_id = 'test-course'
CourseVideos.objects.create(video=video, course_id=self.course_id)
def test_get_video_found(self): def test_get_video_found(self):
""" """
...@@ -149,6 +151,16 @@ class GetVideoInfoTest(TestCase): ...@@ -149,6 +151,16 @@ class GetVideoInfoTest(TestCase):
) )
) )
def test_get_videos_for_course(self):
"""
Tests retrieving videos for a course id
"""
videos = list(api.get_videos_for_course(self.course_id))
self.assertEqual(len(videos), 1)
self.assertEqual(videos[0]['edx_video_id'], constants.VIDEO_DICT_FISH['edx_video_id'])
videos = list(api.get_videos_for_course('unknown'))
self.assertEqual(len(videos), 0)
def test_no_such_video(self): def test_no_such_video(self):
""" """
Tests searching for a video that does not exist Tests searching for a video that does not exist
...@@ -177,7 +189,7 @@ class GetVideoInfoTest(TestCase): ...@@ -177,7 +189,7 @@ class GetVideoInfoTest(TestCase):
constants.VIDEO_DICT_FISH.get("edx_video_id") constants.VIDEO_DICT_FISH.get("edx_video_id")
) )
@mock.patch.object(Video.objects, 'get') @mock.patch.object(Video, '__init__')
def test_force_database_error(self, mock_get): def test_force_database_error(self, mock_get):
""" """
Tests to see if an database error will be handled Tests to see if an database error will be handled
...@@ -225,7 +237,7 @@ class GetVideoInfoTestWithHttpCalls(APIAuthTestCase): ...@@ -225,7 +237,7 @@ class GetVideoInfoTestWithHttpCalls(APIAuthTestCase):
""" """
Tests number of queries for a Video/EncodedVideo(1) pair Tests number of queries for a Video/EncodedVideo(1) pair
""" """
with self.assertNumQueries(7): with self.assertNumQueries(8):
api.get_video_info(constants.COMPLETE_SET_FISH.get("edx_video_id")) api.get_video_info(constants.COMPLETE_SET_FISH.get("edx_video_id"))
def test_get_info_queries_for_one_encoded_video(self): def test_get_info_queries_for_one_encoded_video(self):
...@@ -237,7 +249,7 @@ class GetVideoInfoTestWithHttpCalls(APIAuthTestCase): ...@@ -237,7 +249,7 @@ class GetVideoInfoTestWithHttpCalls(APIAuthTestCase):
url, constants.COMPLETE_SET_STAR, format='json' url, constants.COMPLETE_SET_STAR, format='json'
) )
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(5): with self.assertNumQueries(6):
api.get_video_info(constants.COMPLETE_SET_STAR.get("edx_video_id")) api.get_video_info(constants.COMPLETE_SET_STAR.get("edx_video_id"))
def test_get_info_queries_for_only_video(self): def test_get_info_queries_for_only_video(self):
...@@ -249,6 +261,6 @@ class GetVideoInfoTestWithHttpCalls(APIAuthTestCase): ...@@ -249,6 +261,6 @@ class GetVideoInfoTestWithHttpCalls(APIAuthTestCase):
url, constants.VIDEO_DICT_ZEBRA, format='json' url, constants.VIDEO_DICT_ZEBRA, format='json'
) )
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(3): with self.assertNumQueries(4):
api.get_video_info(constants.VIDEO_DICT_ZEBRA.get("edx_video_id")) api.get_video_info(constants.VIDEO_DICT_ZEBRA.get("edx_video_id"))
...@@ -452,6 +452,35 @@ class VideoListTest(APIAuthTestCase): ...@@ -452,6 +452,35 @@ class VideoListTest(APIAuthTestCase):
errors.get("edx_video_id")[0], errors.get("edx_video_id")[0],
"edx_video_id has invalid characters" "edx_video_id has invalid characters"
) )
def test_add_course(self):
"""
Test adding a video with a course.
Test retrieving videos from course list view.
"""
url = reverse('video-list')
video = dict(**constants.VIDEO_DICT_ANIMAL)
course1 = 'animals/fish'
course2 = 'animals/birds'
video['courses'] = [course1, course2]
response = self.client.post(
url, video, format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
videos = self.client.get("/edxval/video/").data
self.assertEqual(len(videos), 1)
self.assertEqual(videos[0]['courses'], [course1, course2])
url = reverse('course-video-list', kwargs={'course_id': course1})
videos = self.client.get(url).data
self.assertEqual(len(videos), 1)
self.assertEqual(videos[0]['edx_video_id'], constants.VIDEO_DICT_ANIMAL['edx_video_id'])
url = reverse('course-video-list', kwargs={'course_id': course1 + '/bad'})
response = self.client.get(url).data
self.assertEqual(len(response), 0)
# Tests for POST queries to database # Tests for POST queries to database
def test_queries_for_only_video(self): def test_queries_for_only_video(self):
...@@ -459,7 +488,7 @@ class VideoListTest(APIAuthTestCase): ...@@ -459,7 +488,7 @@ class VideoListTest(APIAuthTestCase):
Tests number of queries for a Video with no Encoded Videos Tests number of queries for a Video with no Encoded Videos
""" """
url = reverse('video-list') url = reverse('video-list')
with self.assertNumQueries(8): with self.assertNumQueries(9):
self.client.post(url, constants.VIDEO_DICT_ZEBRA, format='json') self.client.post(url, constants.VIDEO_DICT_ZEBRA, format='json')
def test_queries_for_two_encoded_video(self): def test_queries_for_two_encoded_video(self):
...@@ -467,7 +496,7 @@ class VideoListTest(APIAuthTestCase): ...@@ -467,7 +496,7 @@ class VideoListTest(APIAuthTestCase):
Tests number of queries for a Video/EncodedVideo(2) pair Tests number of queries for a Video/EncodedVideo(2) pair
""" """
url = reverse('video-list') url = reverse('video-list')
with self.assertNumQueries(20): with self.assertNumQueries(21):
self.client.post(url, constants.COMPLETE_SET_FISH, format='json') self.client.post(url, constants.COMPLETE_SET_FISH, format='json')
def test_queries_for_single_encoded_videos(self): def test_queries_for_single_encoded_videos(self):
...@@ -475,7 +504,7 @@ class VideoListTest(APIAuthTestCase): ...@@ -475,7 +504,7 @@ class VideoListTest(APIAuthTestCase):
Tests number of queries for a Video/EncodedVideo(1) pair Tests number of queries for a Video/EncodedVideo(1) pair
""" """
url = reverse('video-list') url = reverse('video-list')
with self.assertNumQueries(14): with self.assertNumQueries(15):
self.client.post(url, constants.COMPLETE_SET_STAR, format='json') self.client.post(url, constants.COMPLETE_SET_STAR, format='json')
...@@ -512,15 +541,15 @@ class VideoDetailTest(APIAuthTestCase): ...@@ -512,15 +541,15 @@ class VideoDetailTest(APIAuthTestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.post(url, constants.VIDEO_DICT_ZEBRA, format='json') response = self.client.post(url, constants.VIDEO_DICT_ZEBRA, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(6): with self.assertNumQueries(7):
self.client.get("/edxval/video/").data self.client.get("/edxval/video/").data
response = self.client.post(url, constants.COMPLETE_SET_FISH, format='json') response = self.client.post(url, constants.COMPLETE_SET_FISH, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(11): with self.assertNumQueries(12):
self.client.get("/edxval/video/").data self.client.get("/edxval/video/").data
response = self.client.post(url, constants.COMPLETE_SET_STAR, format='json') response = self.client.post(url, constants.COMPLETE_SET_STAR, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(14): with self.assertNumQueries(15):
self.client.get("/edxval/video/").data self.client.get("/edxval/video/").data
......
...@@ -28,4 +28,10 @@ urlpatterns = patterns( ...@@ -28,4 +28,10 @@ urlpatterns = patterns(
views.get_subtitle, views.get_subtitle,
name="subtitle-content" name="subtitle-content"
), ),
url(
r'^edxval/course/(?P<course_id>[-\w/]+)$',
views.CourseVideoList.as_view(),
name="course-video-list"
),
) )
...@@ -34,11 +34,21 @@ class VideoList(generics.ListCreateAPIView): ...@@ -34,11 +34,21 @@ class VideoList(generics.ListCreateAPIView):
GETs or POST video objects GETs or POST video objects
""" """
permission_classes = (DjangoModelPermissions,) permission_classes = (DjangoModelPermissions,)
queryset = Video.objects.all().prefetch_related("encoded_videos") queryset = Video.objects.all().prefetch_related("encoded_videos", "courses")
lookup_field = "edx_video_id" lookup_field = "edx_video_id"
serializer_class = VideoSerializer serializer_class = VideoSerializer
class CourseVideoList(generics.ListAPIView):
permission_classes = (DjangoModelPermissions,)
queryset = Video.objects.all().prefetch_related("encoded_videos")
lookup_field = "course_id"
serializer_class = VideoSerializer
def get_queryset(self):
return self.queryset.filter(courses__course_id=self.kwargs['course_id'])
class ProfileList(generics.ListCreateAPIView): class ProfileList(generics.ListCreateAPIView):
""" """
GETs or POST video objects GETs or POST video objects
......
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