From 0a85bb82bf1194484359c1158cafc89defab80f4 Mon Sep 17 00:00:00 2001 From: Brandon Bodine Date: Sun, 17 Sep 2023 22:57:44 -0600 Subject: [PATCH] feat: add endpoint for course transcript details --- edxval/__init__.py | 2 +- edxval/api.py | 45 ++++++++++++++++++++++++++++++++++++++ edxval/tests/test_api.py | 33 ++++++++++++++++++++++++++++ edxval/tests/test_views.py | 26 ++++++++++++++++++++++ edxval/urls.py | 3 +++ edxval/views.py | 19 +++++++++++++++- 6 files changed, 126 insertions(+), 2 deletions(-) diff --git a/edxval/__init__.py b/edxval/__init__.py index 7a5b9868..3b310695 100644 --- a/edxval/__init__.py +++ b/edxval/__init__.py @@ -2,4 +2,4 @@ init """ -__version__ = '2.4.2' +__version__ = '2.4.3' diff --git a/edxval/api.py b/edxval/api.py index f631693c..ad25090a 100644 --- a/edxval/api.py +++ b/edxval/api.py @@ -733,6 +733,51 @@ def get_videos_for_course(course_id, sort_field=None, sort_dir=SortDirection.asc ) +def get_transcript_details_for_course(course_id): + """ + Gets all the transcript for a course and bundles up data. + + Args: + course_id (String) + + Returns: + (dict): Returns all the edx_video_id's and related transcript details for a course + { + 'edx_video_id': { + 'lang_code': { + 'provider': 'What the provider is', + 'content': 'Content of the transcript', + 'file_format': 'file format', + 'url': 'location of the file', + 'name': 'name of the file', + 'size': size of the file + } + } + """ + course_transcripts_data = {} + + course_videos = CourseVideo.objects.filter(course_id=course_id).select_related('video') + for course_video in course_videos: + + edx_video_id = course_video.video.edx_video_id + transcript_data = {} + + video_transcripts = VideoTranscript.objects.filter(video=course_video.video) + for video_transcript in video_transcripts: + transcript_data[video_transcript.language_code] = { + 'provider': video_transcript.provider, + 'content': video_transcript.transcript.file.read(), + 'file_format': video_transcript.file_format, + 'url': video_transcript.transcript.url, + 'name': video_transcript.transcript.name, + 'size': video_transcript.transcript.size, + } + + course_transcripts_data[edx_video_id] = transcript_data + + return course_transcripts_data + + def remove_video_for_course(course_id, edx_video_id): """ Soft deletes video for particular course. diff --git a/edxval/tests/test_api.py b/edxval/tests/test_api.py index 6907f862..12c88a11 100644 --- a/edxval/tests/test_api.py +++ b/edxval/tests/test_api.py @@ -2684,6 +2684,12 @@ def setUp(self): self.v2_transcript1 = video_and_transcripts['transcripts']['de'] self.v2_transcript2 = video_and_transcripts['transcripts']['zh'] + # Add the videos to courses + self.course_id1 = 'test-course-1' + self.course_id2 = 'test-course-2' + CourseVideo.objects.create(video=self.video1, course_id=self.course_id1) + CourseVideo.objects.create(video=self.video2, course_id=self.course_id2) + self.temp_dir = mkdtemp() self.addCleanup(shutil.rmtree, self.temp_dir) @@ -3133,6 +3139,33 @@ def test_no_create_transcript_file(self, video_id, language_code): # Verify no file is created. self.assertEqual(file_system.listdir(constants.EXPORT_IMPORT_STATIC_DIR), []) + def test_get_transcript_details_for_course(self): + """ + Verify that `get_transcript_details_for_course` api function works as expected. + """ + + course_transcript = api.get_transcript_details_for_course(self.course_id1) + + self.assertEqual(course_transcript['super-soaker']['en']['provider'], TranscriptProviderType.THREE_PLAY_MEDIA) + self.assertIn('content', course_transcript['super-soaker']['en']) + self.assertEqual(course_transcript['super-soaker']['en']['file_format'], utils.TranscriptFormat.SRT) + self.assertIn('url', course_transcript['super-soaker']['en']) + self.assertIn('name', course_transcript['super-soaker']['en']) + self.assertIn('size', course_transcript['super-soaker']['en']) + + self.assertEqual(course_transcript['super-soaker']['fr']['provider'], TranscriptProviderType.CIELO24) + self.assertIn('content', course_transcript['super-soaker']['fr']) + self.assertEqual(course_transcript['super-soaker']['en']['file_format'], utils.TranscriptFormat.SRT) + self.assertIn('url', course_transcript['super-soaker']['fr']) + self.assertIn('name', course_transcript['super-soaker']['fr']) + self.assertIn('size', course_transcript['super-soaker']['fr']) + + def test_get_transcript_details_for_course_no_course_videos(self): + + course_transcript = api.get_transcript_details_for_course('this-is-not-a-course-id') + + self.assertEqual(len(course_transcript), 0) + @ddt class TranscriptPreferencesTest(TestCase): diff --git a/edxval/tests/test_views.py b/edxval/tests/test_views.py index 236a9848..2e89402f 100644 --- a/edxval/tests/test_views.py +++ b/edxval/tests/test_views.py @@ -4,6 +4,7 @@ import json +from unittest.mock import patch from ddt import data, ddt, unpack from django.urls import reverse @@ -1101,3 +1102,28 @@ def test_update_hls_encodes_for_video(self): self.assertEqual(actual_encoded_video.url, expected_data['encode_data']['url']) self.assertEqual(actual_encoded_video.file_size, expected_data['encode_data']['file_size']) self.assertEqual(actual_encoded_video.bitrate, expected_data['encode_data']['bitrate']) + + +class CourseTranscriptsDetailViewTest(APIAuthTestCase): + """ + CourseTranscriptsDetailView Tests. + """ + base_url = 'course-transcripts' + + def test_successful_response(self): + """ + Test succesful response from view + """ + with patch( + 'edxval.views.get_transcript_details_for_course' + ) as mock_transcript_details: + # Simulate a return value when the function is called. + mock_transcript_details.return_value = {} + course_id = 'course-v1:edx+1+2023_05' + url = reverse(self.base_url, args=[course_id]) + response = self.client.get(url) + + # Verify the function was called once with course_id + mock_transcript_details.assert_called_once_with(course_id) + + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/edxval/urls.py b/edxval/urls.py index cd48c945..e020ebd0 100644 --- a/edxval/urls.py +++ b/edxval/urls.py @@ -19,6 +19,9 @@ path('videos/missing-hls/', views.HLSMissingVideoView.as_view(), name='hls-missing-video' ), + path('videos/course-transcripts//', views.CourseTranscriptsDetailView.as_view(), + name='course-transcripts' + ), path('videos/video-transcripts/create/', views.VideoTranscriptView.as_view(), name='create-video-transcript' ), diff --git a/edxval/views.py b/edxval/views.py index 1dd6eda3..1d5584d1 100644 --- a/edxval/views.py +++ b/edxval/views.py @@ -14,7 +14,7 @@ from rest_framework.response import Response from rest_framework.views import APIView -from edxval.api import create_or_update_video_transcript +from edxval.api import create_or_update_video_transcript, get_transcript_details_for_course from edxval.models import ( LIST_MAX_ITEMS, CourseVideo, @@ -175,6 +175,23 @@ def post(self, request): return response +class CourseTranscriptsDetailView(APIView): + """ + A view to get the details for all the course transcripts related to a course_id. + """ + authentication_classes = (JwtAuthentication, SessionAuthentication) + + def get(self, _request, course_id): + """ + Returns all transcript data for a course when given a course_id. + """ + if not course_id: + return Response(status=status.HTTP_400_BAD_REQUEST, data={'message': 'course_id param required'}) + + course_data = get_transcript_details_for_course(course_id) + return Response(status=status.HTTP_200_OK, data=course_data) + + class VideoStatusView(APIView): """ A Video View to update the status of a video.