From 8cfd04ff68acfdece546ba92a582aaca4585e563 Mon Sep 17 00:00:00 2001 From: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com> Date: Fri, 27 Oct 2023 09:04:37 -0400 Subject: [PATCH] feat: add drf for studio video page (#33528) --- .../rest_api/v1/serializers/__init__.py | 7 +- .../rest_api/v1/serializers/videos.py | 85 +++++++++ .../contentstore/rest_api/v1/urls.py | 12 ++ .../rest_api/v1/views/__init__.py | 4 +- .../rest_api/v1/views/tests/test_videos.py | 128 +++++++++++++ .../contentstore/rest_api/v1/views/videos.py | 174 +++++++++++++++++- cms/djangoapps/contentstore/utils.py | 86 +++++++++ .../contentstore/video_storage_handlers.py | 97 +++++----- .../contentstore/views/tests/test_videos.py | 14 +- 9 files changed, 543 insertions(+), 64 deletions(-) create mode 100644 cms/djangoapps/contentstore/rest_api/v1/views/tests/test_videos.py diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index a5c349799824..2d65d60139a7 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -14,6 +14,11 @@ ) from .settings import CourseSettingsSerializer from .xblock import XblockSerializer -from .videos import VideoUploadSerializer, VideoImageSerializer +from .videos import ( + CourseVideosSerializer, + VideoUploadSerializer, + VideoImageSerializer, + VideoUsageSerializer +) from .transcripts import TranscriptSerializer from .assets import AssetSerializer diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py index c08856d1b511..657e4339b8cd 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py @@ -11,6 +11,91 @@ class FileSpecSerializer(StrictSerializer): content_type = serializers.ChoiceField(choices=['video/mp4', 'video/webm', 'video/ogg']) +class VideoImageSettingsSerializer(serializers.Serializer): + """Serializer for image settings""" + video_image_upload_enabled = serializers.BooleanField() + max_size = serializers.IntegerField() + min_size = serializers.IntegerField() + max_width = serializers.IntegerField() + max_height = serializers.IntegerField() + supported_file_formats = serializers.DictField( + child=serializers.CharField() + ) + + +class VideoTranscriptSettingsSerializer(serializers.Serializer): + """Serializer for transcript settings""" + transcript_download_handler_url = serializers.CharField() + transcript_upload_handler_url = serializers.CharField() + transcript_delete_handler_url = serializers.CharField() + trancript_download_file_format = serializers.CharField() + transcript_preferences_handler_url = serializers.CharField(required=False, allow_null=True) + transcript_credentials_handler_url = serializers.CharField(required=False, allow_null=True) + transcription_plans = serializers.DictField( + child=serializers.DictField(), + required=False, + allow_null=True, + ) + + +class VideoModelSerializer(serializers.Serializer): + """Serializer for a video""" + client_video_id = serializers.CharField() + course_video_image_url = serializers.CharField() + created = serializers.CharField() + duration = serializers.FloatField() + edx_video_id = serializers.CharField() + error_description = serializers.CharField() + status = serializers.CharField() + file_size = serializers.IntegerField() + download_link = serializers.CharField() + transcript_urls = serializers.DictField( + child=serializers.CharField() + ) + transcription_status = serializers.CharField() + transcripts = serializers.ListField( + child=serializers.CharField() + ) + + +class CourseVideosSerializer(serializers.Serializer): + """Serializer for course videos""" + image_upload_url = serializers.CharField() + video_handler_url = serializers.CharField() + encodings_download_url = serializers.CharField() + default_video_image_url = serializers.CharField() + previous_uploads = VideoModelSerializer(many=True, required=False) + concurrent_upload_limit = serializers.IntegerField() + video_supported_file_formats = serializers.ListField( + child=serializers.CharField() + ) + video_upload_max_file_size = serializers.CharField() + video_image_settings = VideoImageSettingsSerializer(required=True, allow_null=False) + is_video_transcript_enabled = serializers.BooleanField() + active_transcript_preferences = serializers.BooleanField(required=False, allow_null=True) + transcript_credentials = serializers.DictField( + child=serializers.CharField() + ) + transcript_available_languages = serializers.ListField( + child=serializers.DictField( + child=serializers.CharField() + ) + ) + video_transcript_settings = VideoTranscriptSettingsSerializer() + pagination_context = serializers.DictField( + child=serializers.CharField(), + required=False, + allow_null=True, + ) + + +class VideoUsageSerializer(serializers.Serializer): + """Serializer for video usage""" + usage_locations = serializers.ListField( + child=serializers.CharField() + ) + + class VideoUploadSerializer(StrictSerializer): """ Strict Serializer for video upload urls. diff --git a/cms/djangoapps/contentstore/rest_api/v1/urls.py b/cms/djangoapps/contentstore/rest_api/v1/urls.py index d0ead20ffe45..b9f68aa3e982 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v1/urls.py @@ -11,6 +11,7 @@ CourseGradingView, CourseRerunView, CourseSettingsView, + CourseVideosView, HomePageView, ProctoredExamSettingsView, ProctoringErrorsView, @@ -19,6 +20,7 @@ videos, transcripts, HelpUrlsView, + VideoUsageView ) app_name = 'v1' @@ -31,6 +33,16 @@ HomePageView.as_view(), name="home" ), + re_path( + fr'^videos/{COURSE_ID_PATTERN}$', + CourseVideosView.as_view(), + name="course_videos" + ), + re_path( + fr'^videos/{COURSE_ID_PATTERN}/{VIDEO_ID_PATTERN}/usage$', + VideoUsageView.as_view(), + name="video_usage" + ), re_path( fr'^proctored_exam_settings/{COURSE_ID_PATTERN}$', ProctoredExamSettingsView.as_view(), diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py index 2d0da478e506..dfa87a4a34b3 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py @@ -11,10 +11,12 @@ from .xblock import XblockView, XblockCreateView from .assets import AssetsCreateRetrieveView, AssetsUpdateDestroyView from .videos import ( + CourseVideosView, VideosUploadsView, VideosCreateUploadView, VideoImagesView, VideoEncodingsDownloadView, - VideoFeaturesView + VideoFeaturesView, + VideoUsageView, ) from .help_urls import HelpUrlsView diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_videos.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_videos.py new file mode 100644 index 000000000000..d5d277c498dc --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_videos.py @@ -0,0 +1,128 @@ +""" +Unit tests for course settings views. +""" +import ddt +from django.conf import settings +from django.contrib.staticfiles.storage import staticfiles_storage +from django.urls import reverse +from edx_toggles.toggles import WaffleSwitch +from edx_toggles.toggles.testutils import override_waffle_switch +from edxval.api import ( + get_3rd_party_transcription_plans, + get_transcript_credentials_state_for_org, + get_transcript_preferences, +) +from mock import patch +from rest_framework import status + +from cms.djangoapps.contentstore.video_storage_handlers import get_all_transcript_languages +from cms.djangoapps.contentstore.tests.utils import CourseTestCase +from cms.djangoapps.contentstore.utils import reverse_course_url + +from ...mixins import PermissionAccessMixin + + +@ddt.ddt +class CourseVideosViewTest(CourseTestCase, PermissionAccessMixin): + """ + Tests for CourseVideosView. + """ + + def setUp(self): + super().setUp() + self.url = reverse( + "cms.djangoapps.contentstore:v1:course_videos", + kwargs={"course_id": self.course.id}, + ) + + def test_course_videos_response(self): + """Check successful response content""" + response = self.client.get(self.url) + expected_response = { + "image_upload_url": reverse_course_url("video_images_handler", str(self.course.id)), + "video_handler_url": reverse_course_url("videos_handler", str(self.course.id)), + "encodings_download_url": reverse_course_url("video_encodings_download", str(self.course.id)), + "default_video_image_url": staticfiles_storage.url(settings.VIDEO_IMAGE_DEFAULT_FILENAME), + "previous_uploads": [], + "concurrent_upload_limit": settings.VIDEO_UPLOAD_PIPELINE.get("CONCURRENT_UPLOAD_LIMIT", 0), + "video_supported_file_formats": [".mp4", ".mov"], + "video_upload_max_file_size": "5", + "video_image_settings": { + "video_image_upload_enabled": False, + "max_size": settings.VIDEO_IMAGE_SETTINGS["VIDEO_IMAGE_MAX_BYTES"], + "min_size": settings.VIDEO_IMAGE_SETTINGS["VIDEO_IMAGE_MIN_BYTES"], + "max_width": settings.VIDEO_IMAGE_MAX_WIDTH, + "max_height": settings.VIDEO_IMAGE_MAX_HEIGHT, + "supported_file_formats": settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS + }, + "is_video_transcript_enabled": False, + "active_transcript_preferences": None, + "transcript_credentials": None, + "transcript_available_languages": get_all_transcript_languages(), + "video_transcript_settings": { + "transcript_download_handler_url": reverse('transcript_download_handler'), + "transcript_upload_handler_url": reverse('transcript_upload_handler'), + "transcript_delete_handler_url": reverse_course_url("transcript_delete_handler", str(self.course.id)), + "trancript_download_file_format": "srt", + "transcript_preferences_handler_url": None, + "transcript_credentials_handler_url": None, + "transcription_plans": None + }, + "pagination_context": {} + } + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertDictEqual(expected_response, response.data) + + @override_waffle_switch(WaffleSwitch( # lint-amnesty, pylint: disable=toggle-missing-annotation + 'videos.video_image_upload_enabled', __name__ + ), True) + def test_video_image_upload_enabled(self): + """ + Make sure if the feature flag is enabled we have updated the dict keys in response. + """ + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("video_image_settings", response.data) + + imageSettings = response.data["video_image_settings"] + self.assertIn("video_image_upload_enabled", imageSettings) + self.assertTrue(imageSettings["video_image_upload_enabled"]) + + def test_VideoTranscriptEnabledFlag_enabled(self): + """ + Make sure if the feature flags are enabled we have updated the dict keys in response. + """ + with patch('openedx.core.djangoapps.video_config.models.VideoTranscriptEnabledFlag.feature_enabled') as feature: + feature.return_value = True + response = self.client.get(self.url) + self.assertIn("is_video_transcript_enabled", response.data) + self.assertTrue(response.data["is_video_transcript_enabled"]) + + expect_active_preferences = get_transcript_preferences(str(self.course.id)) + self.assertIn("active_transcript_preferences", response.data) + self.assertEqual(expect_active_preferences, response.data["active_transcript_preferences"]) + + expected_credentials = get_transcript_credentials_state_for_org(self.course.id.org) + self.assertIn("transcript_credentials", response.data) + self.assertDictEqual(expected_credentials, response.data["transcript_credentials"]) + + transcript_settings = response.data["video_transcript_settings"] + + expected_plans = get_3rd_party_transcription_plans() + self.assertIn("transcription_plans", transcript_settings) + self.assertDictEqual(expected_plans, transcript_settings["transcription_plans"]) + + expected_preference_handler = reverse_course_url( + 'transcript_preferences_handler', + str(self.course.id) + ) + self.assertIn("transcript_preferences_handler_url", transcript_settings) + self.assertEqual(expected_preference_handler, transcript_settings["transcript_preferences_handler_url"]) + + expected_credentials_handler = reverse_course_url( + 'transcript_credentials_handler', + str(self.course.id) + ) + self.assertIn("transcript_credentials_handler_url", transcript_settings) + self.assertEqual(expected_credentials_handler, transcript_settings["transcript_credentials_handler_url"]) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py index 417490692fe1..d4ea444bd134 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py @@ -1,29 +1,42 @@ """ Public rest API endpoints for the CMS API video assets. """ +import edx_api_doc_tools as apidocs import logging +from opaque_keys.edx.keys import CourseKey from rest_framework.generics import ( CreateAPIView, RetrieveAPIView, DestroyAPIView ) +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView from rest_framework.parsers import (MultiPartParser, FormParser) from django.views.decorators.csrf import csrf_exempt from django.http import Http404 -from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes +from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes, verify_course_exists from openedx.core.lib.api.parsers import TypedFileUploadParser +from common.djangoapps.student.auth import has_studio_read_access from common.djangoapps.util.json_request import expect_json_in_class_view from ....api import course_author_access_required +from ....utils import get_course_videos_context from cms.djangoapps.contentstore.video_storage_handlers import ( handle_videos, get_video_encodings_download, handle_video_images, - enabled_video_features + enabled_video_features, + get_video_usage_path +) +from cms.djangoapps.contentstore.rest_api.v1.serializers import ( + CourseVideosSerializer, + VideoUploadSerializer, + VideoImageSerializer, + VideoUsageSerializer, ) -from cms.djangoapps.contentstore.rest_api.v1.serializers import VideoUploadSerializer, VideoImageSerializer import cms.djangoapps.contentstore.toggles as contentstore_toggles from .utils import validate_request_with_serializer @@ -32,6 +45,161 @@ toggles = contentstore_toggles +@view_auth_classes(is_authenticated=True) +class CourseVideosView(DeveloperErrorViewMixin, APIView): + """ + View for course videos. + """ + @apidocs.schema( + parameters=[ + apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"), + ], + responses={ + 200: CourseVideosSerializer, + 401: "The requester is not authenticated", + 403: "The requester cannot access the specified course", + 404: "The requested course does not exist", + }, + ) + @verify_course_exists() + def get(self, request: Request, course_id: str): + """ + Get an object containing course videos. + **Example Request** + GET /api/contentstore/v1/videos/{course_id}/{edx_video_id} + **Response Values** + If the request is successful, an HTTP 200 "OK" response is returned. + The HTTP 200 response contains a single dict that contains keys that + are the course's videos. + **Example Response** + ```json + { + image_upload_url: '/video_images/course_id', + video_handler_url: '/videos/course_id', + encodings_download_url: '/video_encodings_download/course_id', + default_video_image_url: '/static/studio/images/video-images/default_video_image.png', + previous_uploads: [ + { + edx_video_id: 'mOckID1', + clientVideoId: 'mOckID1.mp4', + created: '', + courseVideoImageUrl: '/video', + transcripts: [], + status: 'Imported', + file_size: 123, + download_link: 'http:/download_video.com' + }, + { + edx_video_id: 'mOckID5', + clientVideoId: 'mOckID5.mp4', + created: '', + courseVideoImageUrl: 'http:/video', + transcripts: ['en'], + status: 'Failed', + file_size: 0, + download_link: '' + }, + { + edx_video_id: 'mOckID3', + clientVideoId: 'mOckID3.mp4', + created: '', + courseVideoImageUrl: null, + transcripts: ['en'], + status: 'Ready', + file_size: 123, + download_link: 'http:/download_video.com' + }, + ], + concurrent_upload_limit: 4, + video_supported_file_formats: ['.mp4', '.mov'], + video_upload_max_file_size: '5', + video_image_settings: { + video_image_upload_enabled: false, + max_size: 2097152, + min_size: 2048, + max_width: 1280, + max_height: 720, + supported_file_formats: { + '.bmp': 'image/bmp', + '.bmp2': 'image/x-ms-bmp', + '.gif': 'image/gif', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + }, + }, + is_video_transcript_enabled: false, + active_transcript_preferences: null, + transcript_credentials: {}, + transcript_available_languages: [{ language_code: 'ab', language_text: 'Abkhazian' }], + video_transcript_settings: { + transcript_download_handler_url: '/transcript_download/', + transcript_upload_handler_url: '/transcript_upload/', + transcript_delete_handler_url: '/transcript_delete/course_id', + trancript_download_file_format: 'srt', + }, + pagination_context: {}, + } + ``` + """ + course_key = CourseKey.from_string(course_id) + + if not has_studio_read_access(request.user, course_key): + self.permission_denied(request) + + course_videos_context = get_course_videos_context( + None, + None, + course_key, + ) + serializer = CourseVideosSerializer(course_videos_context) + return Response(serializer.data) + + +@view_auth_classes(is_authenticated=True) +class VideoUsageView(DeveloperErrorViewMixin, APIView): + """ + View for course video usage locations. + """ + @apidocs.schema( + parameters=[ + apidocs.string_parameter("course_id", apidocs.ParameterLocation.PATH, description="Course ID"), + apidocs.string_parameter("edx_video_id", apidocs.ParameterLocation.PATH, description="edX Video ID"), + ], + responses={ + 200: VideoUsageSerializer, + 401: "The requester is not authenticated", + 403: "The requester cannot access the specified course", + 404: "The requested course does not exist", + }, + ) + @verify_course_exists() + def get(self, request: Request, course_id: str, edx_video_id: str): + """ + Get an object containing course videos. + **Example Request** + GET /api/contentstore/v1/videos/{course_id}/{edx_video_id} + **Response Values** + If the request is successful, an HTTP 200 "OK" response is returned. + The HTTP 200 response contains a single dict that contains keys that + are the course's videos. + **Example Response** + ```json + { + "usage_locations": ["subsection - unit/xblock"], + } + ``` + """ + course_key = CourseKey.from_string(course_id) + + if not has_studio_read_access(request.user, course_key): + self.permission_denied(request) + + usage_locations = get_video_usage_path(request, course_key, edx_video_id) + serializer = VideoUsageSerializer(usage_locations) + return Response(serializer.data) + + @view_auth_classes() class VideosUploadsView(DeveloperErrorViewMixin, RetrieveAPIView, DestroyAPIView): """ diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 83c26197a4a7..48fc2c962cf1 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1568,6 +1568,92 @@ def get_course_rerun_context(course_key, course_block, user): return course_rerun_context +def get_course_videos_context(course_block, pagination_conf, course_key=None): + """ + Utils is used to get contest of course videos. + It is used for both DRF and django views. + """ + + from edx_toggles.toggles import WaffleSwitch + from edxval.api import ( + get_3rd_party_transcription_plans, + get_transcript_credentials_state_for_org, + get_transcript_preferences, + ) + from openedx.core.djangoapps.video_config.models import VideoTranscriptEnabledFlag + from xmodule.video_block.transcripts_utils import Transcript # lint-amnesty, pylint: disable=wrong-import-order + + from .video_storage_handlers import ( + get_all_transcript_languages, + _get_index_videos, + _get_default_video_image_url + ) + + VIDEO_SUPPORTED_FILE_FORMATS = { + '.mp4': 'video/mp4', + '.mov': 'video/quicktime', + } + VIDEO_UPLOAD_MAX_FILE_SIZE_GB = 5 + # Waffle switch for enabling/disabling video image upload feature + VIDEO_IMAGE_UPLOAD_ENABLED = WaffleSwitch( # lint-amnesty, pylint: disable=toggle-missing-annotation + 'videos.video_image_upload_enabled', __name__ + ) + + course = course_block + if not course: + with modulestore().bulk_operations(course_key): + course = modulestore().get_course(course_key) + + is_video_transcript_enabled = VideoTranscriptEnabledFlag.feature_enabled(course.id) + previous_uploads, pagination_context = _get_index_videos(course, pagination_conf) + course_video_context = { + 'context_course': course, + 'image_upload_url': reverse_course_url('video_images_handler', str(course.id)), + 'video_handler_url': reverse_course_url('videos_handler', str(course.id)), + 'encodings_download_url': reverse_course_url('video_encodings_download', str(course.id)), + 'default_video_image_url': _get_default_video_image_url(), + 'previous_uploads': previous_uploads, + 'concurrent_upload_limit': settings.VIDEO_UPLOAD_PIPELINE.get('CONCURRENT_UPLOAD_LIMIT', 0), + 'video_supported_file_formats': list(VIDEO_SUPPORTED_FILE_FORMATS.keys()), + 'video_upload_max_file_size': VIDEO_UPLOAD_MAX_FILE_SIZE_GB, + 'video_image_settings': { + 'video_image_upload_enabled': VIDEO_IMAGE_UPLOAD_ENABLED.is_enabled(), + 'max_size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MAX_BYTES'], + 'min_size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MIN_BYTES'], + 'max_width': settings.VIDEO_IMAGE_MAX_WIDTH, + 'max_height': settings.VIDEO_IMAGE_MAX_HEIGHT, + 'supported_file_formats': settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS + }, + 'is_video_transcript_enabled': is_video_transcript_enabled, + 'active_transcript_preferences': None, + 'transcript_credentials': None, + 'transcript_available_languages': get_all_transcript_languages(), + 'video_transcript_settings': { + 'transcript_download_handler_url': reverse('transcript_download_handler'), + 'transcript_upload_handler_url': reverse('transcript_upload_handler'), + 'transcript_delete_handler_url': reverse_course_url('transcript_delete_handler', str(course.id)), + 'trancript_download_file_format': Transcript.SRT + }, + 'pagination_context': pagination_context + } + if is_video_transcript_enabled: + course_video_context['video_transcript_settings'].update({ + 'transcript_preferences_handler_url': reverse_course_url( + 'transcript_preferences_handler', + str(course.id) + ), + 'transcript_credentials_handler_url': reverse_course_url( + 'transcript_credentials_handler', + str(course.id) + ), + 'transcription_plans': get_3rd_party_transcription_plans(), + }) + course_video_context['active_transcript_preferences'] = get_transcript_preferences(str(course.id)) + # Cached state for transcript providers' credentials (org-specific) + course_video_context['transcript_credentials'] = get_transcript_credentials_state_for_org(course.id.org) + return course_video_context + + class StudioPermissionsService: """ Service that can provide information about a user's permissions. diff --git a/cms/djangoapps/contentstore/video_storage_handlers.py b/cms/djangoapps/contentstore/video_storage_handlers.py index 6e83a5b9e3cc..318a91b4bb79 100644 --- a/cms/djangoapps/contentstore/video_storage_handlers.py +++ b/cms/djangoapps/contentstore/video_storage_handlers.py @@ -15,9 +15,9 @@ from boto import s3 from django.conf import settings from django.contrib.staticfiles.storage import staticfiles_storage +from django.core.exceptions import PermissionDenied from django.http import FileResponse, HttpResponseNotFound from django.shortcuts import redirect -from django.urls import reverse from django.utils.translation import gettext as _ from django.utils.translation import gettext_noop from edx_toggles.toggles import WaffleSwitch @@ -29,7 +29,6 @@ get_3rd_party_transcription_plans, get_available_transcript_languages, get_video_transcript_url, - get_transcript_credentials_state_for_org, get_transcript_preferences, get_videos_for_course, remove_transcript_preferences, @@ -43,6 +42,7 @@ from rest_framework.response import Response from common.djangoapps.edxmako.shortcuts import render_to_response +from common.djangoapps.student.auth import has_course_author_access from common.djangoapps.util.json_request import JsonResponse from openedx.core.djangoapps.video_config.models import VideoTranscriptEnabledFlag from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE @@ -51,11 +51,11 @@ ENABLE_DEVSTACK_VIDEO_UPLOADS, ) from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag -from xmodule.video_block.transcripts_utils import Transcript # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from .models import VideoUploadConfig from .toggles import use_new_video_uploads_page, use_mock_video_uploads -from .utils import reverse_course_url, get_video_uploads_url +from .utils import get_video_uploads_url, get_course_videos_context from .video_utils import validate_video_image from .views.course import get_course_and_check_access @@ -223,6 +223,33 @@ def handle_videos(request, course_key_string, edx_video_id=None): return JsonResponse(data, status=status) +def get_video_usage_path(request, course_key, edx_video_id): + """ + API for fetching the locations a specific video is used in a course. + Returns a list of paths to a video. + """ + if not has_course_author_access(request.user, course_key): + raise PermissionDenied() + store = modulestore() + usage_locations = [] + videos = store.get_items( + course_key, + qualifiers={ + 'category': 'video' + }, + ) + for video in videos: + video_id = getattr(video, 'edx_video_id', '') + if video_id == edx_video_id: + unit = video.get_parent() + subsection = unit.get_parent() + subsection_display_name = getattr(subsection, 'display_name', '') + unit_display_name = getattr(unit, 'display_name', '') + xblock_display_name = getattr(video, 'display_name', '') + usage_locations.append(f'{subsection_display_name} - {unit_display_name} / {xblock_display_name}') + return {'usage_locations': usage_locations} + + def handle_generate_video_upload_link(request, course_key_string): """ API for creating a video upload. Returns an edx_video_id and a presigned URL that can be used @@ -585,7 +612,7 @@ def _get_index_videos(course, pagination_conf=None): course_id = str(course.id) attrs = [ 'edx_video_id', 'client_video_id', 'created', 'duration', - 'status', 'courses', 'transcripts', 'transcription_status', + 'status', 'courses', 'encoded_videos', 'transcripts', 'transcription_status', 'transcript_urls', 'error_description' ] @@ -598,9 +625,15 @@ def _get_values(video): if attr == 'courses': course = [c for c in video['courses'] if course_id in c] (__, values['course_video_image_url']), = list(course[0].items()) + elif attr == 'encoded_videos': + values['download_link'] = '' + values['file_size'] = 0 + for encoding in video['encoded_videos']: + if encoding['profile'] == 'desktop_mp4': + values['download_link'] = encoding['url'] + values['file_size'] = encoding['file_size'] else: values[attr] = video[attr] - return values videos, pagination_context = _get_videos(course, pagination_conf) @@ -636,54 +669,10 @@ def videos_index_html(course, pagination_conf=None): """ Returns an HTML page to display previous video uploads and allow new ones """ - is_video_transcript_enabled = VideoTranscriptEnabledFlag.feature_enabled(course.id) - previous_uploads, pagination_context = _get_index_videos(course, pagination_conf) - context = { - 'context_course': course, - 'image_upload_url': reverse_course_url('video_images_handler', str(course.id)), - 'video_handler_url': reverse_course_url('videos_handler', str(course.id)), - 'encodings_download_url': reverse_course_url('video_encodings_download', str(course.id)), - 'default_video_image_url': _get_default_video_image_url(), - 'previous_uploads': previous_uploads, - 'concurrent_upload_limit': settings.VIDEO_UPLOAD_PIPELINE.get('CONCURRENT_UPLOAD_LIMIT', 0), - 'video_supported_file_formats': list(VIDEO_SUPPORTED_FILE_FORMATS.keys()), - 'video_upload_max_file_size': VIDEO_UPLOAD_MAX_FILE_SIZE_GB, - 'video_image_settings': { - 'video_image_upload_enabled': VIDEO_IMAGE_UPLOAD_ENABLED.is_enabled(), - 'max_size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MAX_BYTES'], - 'min_size': settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MIN_BYTES'], - 'max_width': settings.VIDEO_IMAGE_MAX_WIDTH, - 'max_height': settings.VIDEO_IMAGE_MAX_HEIGHT, - 'supported_file_formats': settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS - }, - 'is_video_transcript_enabled': is_video_transcript_enabled, - 'active_transcript_preferences': None, - 'transcript_credentials': None, - 'transcript_available_languages': get_all_transcript_languages(), - 'video_transcript_settings': { - 'transcript_download_handler_url': reverse('transcript_download_handler'), - 'transcript_upload_handler_url': reverse('transcript_upload_handler'), - 'transcript_delete_handler_url': reverse_course_url('transcript_delete_handler', str(course.id)), - 'trancript_download_file_format': Transcript.SRT - }, - 'pagination_context': pagination_context - } - - if is_video_transcript_enabled: - context['video_transcript_settings'].update({ - 'transcript_preferences_handler_url': reverse_course_url( - 'transcript_preferences_handler', - str(course.id) - ), - 'transcript_credentials_handler_url': reverse_course_url( - 'transcript_credentials_handler', - str(course.id) - ), - 'transcription_plans': get_3rd_party_transcription_plans(), - }) - context['active_transcript_preferences'] = get_transcript_preferences(str(course.id)) - # Cached state for transcript providers' credentials (org-specific) - context['transcript_credentials'] = get_transcript_credentials_state_for_org(course.id.org) + context = get_course_videos_context( + course, + pagination_conf, + ) if use_new_video_uploads_page(course.id): return redirect(get_video_uploads_url(course.id)) return render_to_response('videos_index.html', context) diff --git a/cms/djangoapps/contentstore/views/tests/test_videos.py b/cms/djangoapps/contentstore/views/tests/test_videos.py index c0f48073ad74..7d2f6e36e524 100644 --- a/cms/djangoapps/contentstore/views/tests/test_videos.py +++ b/cms/djangoapps/contentstore/views/tests/test_videos.py @@ -358,6 +358,7 @@ def test_get_json(self): for i, response_video in enumerate(response_videos): # Videos should be returned by creation date descending original_video = self.previous_uploads[-(i + 1)] + print(response_video.keys()) self.assertEqual( set(response_video.keys()), { @@ -367,6 +368,8 @@ def test_get_json(self): 'duration', 'status', 'course_video_image_url', + 'file_size', + 'download_link', 'transcripts', 'transcription_status', 'transcript_urls', @@ -385,8 +388,8 @@ def test_get_json(self): ( [ 'edx_video_id', 'client_video_id', 'created', 'duration', - 'status', 'course_video_image_url', 'transcripts', 'transcription_status', - 'transcript_urls', 'error_description' + 'status', 'course_video_image_url', 'file_size', 'download_link', + 'transcripts', 'transcription_status', 'transcript_urls', 'error_description' ], [ { @@ -402,8 +405,8 @@ def test_get_json(self): ( [ 'edx_video_id', 'client_video_id', 'created', 'duration', - 'status', 'course_video_image_url', 'transcripts', 'transcription_status', - 'transcript_urls', 'error_description' + 'status', 'course_video_image_url', 'file_size', 'download_link', + 'transcripts', 'transcription_status', 'transcript_urls', 'error_description' ], [ { @@ -444,8 +447,9 @@ def test_get_json_transcripts(self, expected_video_keys, uploaded_transcripts, e self.assertEqual(response.status_code, 200) response_videos = json.loads(response.content.decode('utf-8'))['videos'] self.assertEqual(len(response_videos), len(self.previous_uploads)) - for response_video in response_videos: + print(response_video) + self.assertEqual(set(response_video.keys()), set(expected_video_keys)) if response_video['edx_video_id'] == self.previous_uploads[0]['edx_video_id']: self.assertEqual(response_video.get('transcripts', []), expected_transcripts)