From f98d88f377b7a24e6df65aa39cff5895e996e89c Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Tue, 17 Oct 2023 15:30:36 -0400 Subject: [PATCH 01/11] feat: add drf for studio video page --- .../rest_api/v1/serializers/__init__.py | 8 +- .../rest_api/v1/serializers/videos.py | 85 +++++++- .../contentstore/rest_api/v1/urls.py | 18 ++ .../rest_api/v1/views/__init__.py | 5 +- .../contentstore/rest_api/v1/views/videos.py | 190 +++++++++++++++++- cms/djangoapps/contentstore/utils.py | 78 +++++++ .../contentstore/video_storage_handlers.py | 138 ++++++++----- 7 files changed, 465 insertions(+), 57 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index a5c349799824..f339c62bb600 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -14,6 +14,12 @@ ) from .settings import CourseSettingsSerializer from .xblock import XblockSerializer -from .videos import VideoUploadSerializer, VideoImageSerializer +from .videos import ( + CourseVideosSerializer, + VideoUploadSerializer, + VideoImageSerializer, + VideoDownloadSerializer, + 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..ecb4f9a091e0 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py @@ -2,7 +2,7 @@ API Serializers for videos """ from rest_framework import serializers -from .common import StrictSerializer +from .common import StrictSerializer, CourseCommonSerializer class FileSpecSerializer(StrictSerializer): @@ -11,6 +11,89 @@ class FileSpecSerializer(StrictSerializer): content_type = serializers.ChoiceField(choices=['video/mp4', 'video/webm', 'video/ogg']) +class VideoImageSettingsSerializer(serializers.Serializer): + 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): + 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): + 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() + transcript_urls = serializers.DictField( + child=serializers.CharField() + ) + transcription_status = serializers.CharField() + transcripts = serializers.ListField( + child=serializers.CharField() + ) + +class CourseVideosSerializer(serializers.Serializer): + """Serializer for course home""" + # context_course = CourseCommonSerializer() + 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 VideoDownloadSerializer(serializers.Serializer): + download_link = serializers.CharField() + + +class VideoUsageSerializer(serializers.Serializer): + 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..2dabca571718 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,8 @@ videos, transcripts, HelpUrlsView, + VideoDownloadView, + VideoUsageView ) app_name = 'v1' @@ -31,6 +34,21 @@ 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}$', + VideoDownloadView.as_view(), + name="video_download" + ), + 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..c78e6eda145f 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py @@ -11,10 +11,13 @@ from .xblock import XblockView, XblockCreateView from .assets import AssetsCreateRetrieveView, AssetsUpdateDestroyView from .videos import ( + CourseVideosView, VideosUploadsView, VideosCreateUploadView, VideoImagesView, VideoEncodingsDownloadView, - VideoFeaturesView + VideoFeaturesView, + VideoDownloadView, + VideoUsageView, ) from .help_urls import HelpUrlsView diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py index 417490692fe1..2141680d13f8 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py @@ -1,29 +1,50 @@ """ 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.conf import settings +from django.contrib.staticfiles.storage import staticfiles_storage 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 xmodule.modulestore.django import modulestore 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_index_videos, + generate_video_download_link, + get_video_usage_path +) +from cms.djangoapps.contentstore.rest_api.v1.serializers import ( + CourseVideosSerializer, + VideoUploadSerializer, + VideoImageSerializer, + VideoDownloadSerializer, + VideoUsageSerializer, ) -from cms.djangoapps.contentstore.rest_api.v1.serializers import VideoUploadSerializer, VideoImageSerializer +from cms.djangoapps.contentstore.video_storage_handlers import get_all_transcript_languages import cms.djangoapps.contentstore.toggles as contentstore_toggles from .utils import validate_request_with_serializer @@ -32,6 +53,169 @@ 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 + { + "allow_unicode_course_id": False, + "course_creator_status": "granted", + "number": "101", + "display_name": "new edx course", + "org": "edx", + "run": "2023", + } + ``` + """ + course_key = CourseKey.from_string(course_id) + + if not has_studio_read_access(request.user, course_key): + self.permission_denied(request) + + transcript_languages = get_all_transcript_languages() + default_video_image_url = staticfiles_storage.url(settings.VIDEO_IMAGE_DEFAULT_FILENAME) + with modulestore().bulk_operations(course_key): + course_block = modulestore().get_course(course_key) + videos = _get_index_videos(course_block) + course_videos_context = get_course_videos_context(course_block, transcript_languages, videos, default_video_image_url) + del course_videos_context['context_course'] + serializer = CourseVideosSerializer(course_videos_context) + return Response(serializer.data) + + +@view_auth_classes(is_authenticated=True) +class VideoDownloadView(DeveloperErrorViewMixin, APIView): + """ + View for course video download. + """ + @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: VideoDownloadSerializer, + 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 + { + "download_link": "http://test-download.com/id", + } + ``` + """ + course_key = CourseKey.from_string(course_id) + + if not has_studio_read_access(request.user, course_key): + self.permission_denied(request) + + download_link = generate_video_download_link(request, course_key, edx_video_id) + serializer = VideoDownloadSerializer(download_link) + 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) + print('LOCATION!!!!! \n', usage_locations) + serializer = VideoUsageSerializer(usage_locations) + return Response(serializer.data) + + +@view_auth_classes() +class TranscriptCredentialsView(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", + }, + ) + def post(self, request: Request, course_id: str): + pass + + @view_auth_classes() class VideosUploadsView(DeveloperErrorViewMixin, RetrieveAPIView, DestroyAPIView): """ diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 1a4b709622e6..8437eb5fb124 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1553,6 +1553,84 @@ def get_course_rerun_context(course_key, course_block, user): return course_rerun_context +def get_course_videos_context(course_block, transcript_languages, videos, default_video_image_url): + """ + 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 + + 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__ + ) + + is_video_transcript_enabled = VideoTranscriptEnabledFlag.feature_enabled(course_block.id) + previous_uploads, pagination_context = videos + course_video_context = { + 'context_course': course_block, + 'image_upload_url': reverse_course_url('video_images_handler', str(course_block.id)), + 'video_handler_url': reverse_course_url('videos_handler', str(course_block.id)), + 'encodings_download_url': reverse_course_url('video_encodings_download', str(course_block.id)), + 'default_video_image_url': 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': 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_block.id)), + 'trancript_download_file_format': Transcript.SRT + }, + 'pagination_context': pagination_context + } + print('got contessxt') + if is_video_transcript_enabled: + print('transcripts enabled') + course_video_context['video_transcript_settings'].update({ + 'transcript_preferences_handler_url': reverse_course_url( + 'transcript_preferences_handler', + str(course_block.id) + ), + 'transcript_credentials_handler_url': reverse_course_url( + 'transcript_credentials_handler', + str(course_block.id) + ), + 'transcription_plans': get_3rd_party_transcription_plans(), + }) + course_video_context['active_transcript_preferences'] = get_transcript_preferences(str(course_block.id)) + # Cached state for transcript providers' credentials (org-specific) + course_video_context['transcript_credentials'] = get_transcript_credentials_state_for_org(course_block.id.org) + print(course_video_context['video_transcript_settings']) + 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..f10528f10cbb 100644 --- a/cms/djangoapps/contentstore/video_storage_handlers.py +++ b/cms/djangoapps/contentstore/video_storage_handlers.py @@ -15,6 +15,7 @@ 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 @@ -29,7 +30,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 +43,8 @@ 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.xblock_django.constants import ATTR_KEY_REQUEST_COUNTRY_CODE 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 +53,13 @@ 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 xmodule.video_block.video_utils import rewrite_video_url # 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 reverse_course_url, get_video_uploads_url, get_course_videos_context from .video_utils import validate_video_image from .views.course import get_course_and_check_access @@ -222,6 +226,77 @@ def handle_videos(request, course_key_string, edx_video_id=None): data, status = videos_post(course, request) return JsonResponse(data, status=status) + +def get_video_usage_path(request, course_key, edx_video_id): + 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 generate_video_download_link(request, course_key, edx_video_id): + try: + import edxval.api as edxval_api + except ImportError: + edxval_api = None + download_video_link = '' + # Determine if there is an alternative source for this video + # based on user locale. This exists to support cases where + # we leverage a geography specific CDN, like China. + default_cdn_url = getattr(settings, 'VIDEO_CDN_URL', {}).get('default') + print(request.user, request) + # user_location = request.user.get_current_user().opt_attrs[ATTR_KEY_REQUEST_COUNTRY_CODE] + cdn_url = getattr(settings, 'VIDEO_CDN_URL', {}).get(None, default_cdn_url) + if edx_video_id and edxval_api: # lint-amnesty, pylint: disable=too-many-nested-blocks + try: + val_profiles = ["youtube", "desktop_webm", "desktop_mp4"] + + # if HLSPlaybackEnabledFlag.feature_enabled(self.course_id): + # val_profiles.append('hls') + + # strip edx_video_id to prevent ValVideoNotFoundError error if unwanted spaces are there. TNL-5769 + val_video_urls = edxval_api.get_urls_for_profiles(edx_video_id.strip(), val_profiles) + + # VAL will always give us the keys for the profiles we asked for, but + # if it doesn't have an encoded video entry for that Video + Profile, the + # value will map to `None` + + # add the non-youtube urls to the list of alternative sources + # use the last non-None non-youtube non-hls url as the link to download the video + for url in [val_video_urls[p] for p in val_profiles if p != "youtube"]: + if url: + # don't include hls urls for download + if not url.endswith('.m3u8'): + # function returns None when the url cannot be re-written + rewritten_link = rewrite_video_url(cdn_url, url) + if rewritten_link: + download_video_link = rewritten_link + else: + download_video_link = url + + except (edxval_api.ValInternalError, edxval_api.ValVideoNotFoundError): + # VAL raises this exception if it can't find data for the edx video ID. This can happen if the + # course data is ported to a machine that does not have the VAL data. So for now, pass on this + # exception and fallback to whatever we find in the VideoBlock. + log.warning("Could not retrieve information from VAL for edx Video ID: %s.", edx_video_id) + return {"download_link": download_video_link} + def handle_generate_video_upload_link(request, course_key_string): """ @@ -636,54 +711,15 @@ 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) + videos = _get_index_videos(course, pagination_conf) + transcript_languages = get_all_transcript_languages() + default_video_image_url = _get_default_video_image_url() + context = get_course_videos_context( + course, + transcript_languages, + videos, + default_video_image_url, + ) if use_new_video_uploads_page(course.id): return redirect(get_video_uploads_url(course.id)) return render_to_response('videos_index.html', context) From 48e92fef053840db9be2ce2a1ea0394bc55aed1b Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Wed, 18 Oct 2023 11:30:44 -0400 Subject: [PATCH 02/11] fix: lint errors --- .../rest_api/v1/serializers/__init__.py | 12 ++--- .../rest_api/v1/serializers/videos.py | 13 +++-- .../contentstore/rest_api/v1/views/videos.py | 48 +++++++------------ cms/djangoapps/contentstore/utils.py | 3 -- .../contentstore/video_storage_handlers.py | 30 ++++++++---- 5 files changed, 51 insertions(+), 55 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index f339c62bb600..439e871aaf37 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -15,11 +15,11 @@ from .settings import CourseSettingsSerializer from .xblock import XblockSerializer from .videos import ( - CourseVideosSerializer, - VideoUploadSerializer, - VideoImageSerializer, - VideoDownloadSerializer, - VideoUsageSerializer -) + CourseVideosSerializer, + VideoUploadSerializer, + VideoImageSerializer, + VideoDownloadSerializer, + 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 ecb4f9a091e0..734b1947f110 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py @@ -2,7 +2,7 @@ API Serializers for videos """ from rest_framework import serializers -from .common import StrictSerializer, CourseCommonSerializer +from .common import StrictSerializer class FileSpecSerializer(StrictSerializer): @@ -12,6 +12,7 @@ class FileSpecSerializer(StrictSerializer): class VideoImageSettingsSerializer(serializers.Serializer): + """Serializer for image settings""" video_image_upload_enabled = serializers.BooleanField() max_size = serializers.IntegerField() min_size = serializers.IntegerField() @@ -23,6 +24,7 @@ class VideoImageSettingsSerializer(serializers.Serializer): 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() @@ -30,13 +32,14 @@ class VideoTranscriptSettingsSerializer(serializers.Serializer): 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(), + 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() @@ -52,9 +55,9 @@ class VideoModelSerializer(serializers.Serializer): child=serializers.CharField() ) + class CourseVideosSerializer(serializers.Serializer): - """Serializer for course home""" - # context_course = CourseCommonSerializer() + """Serializer for course videos""" image_upload_url = serializers.CharField() video_handler_url = serializers.CharField() encodings_download_url = serializers.CharField() @@ -85,10 +88,12 @@ class CourseVideosSerializer(serializers.Serializer): class VideoDownloadSerializer(serializers.Serializer): + """Serializer for video download""" download_link = serializers.CharField() class VideoUsageSerializer(serializers.Serializer): + """Serializer for video usage""" usage_locations = serializers.ListField( child=serializers.CharField() ) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py index 2141680d13f8..fc08db759bfd 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py @@ -38,11 +38,11 @@ get_video_usage_path ) from cms.djangoapps.contentstore.rest_api.v1.serializers import ( - CourseVideosSerializer, - VideoUploadSerializer, - VideoImageSerializer, - VideoDownloadSerializer, - VideoUsageSerializer, + CourseVideosSerializer, + VideoUploadSerializer, + VideoImageSerializer, + VideoDownloadSerializer, + VideoUsageSerializer, ) from cms.djangoapps.contentstore.video_storage_handlers import get_all_transcript_languages import cms.djangoapps.contentstore.toggles as contentstore_toggles @@ -94,14 +94,19 @@ def get(self, request: Request, course_id: str): course_key = CourseKey.from_string(course_id) if not has_studio_read_access(request.user, course_key): - self.permission_denied(request) + self.permission_denied(request) transcript_languages = get_all_transcript_languages() default_video_image_url = staticfiles_storage.url(settings.VIDEO_IMAGE_DEFAULT_FILENAME) with modulestore().bulk_operations(course_key): course_block = modulestore().get_course(course_key) videos = _get_index_videos(course_block) - course_videos_context = get_course_videos_context(course_block, transcript_languages, videos, default_video_image_url) + course_videos_context = get_course_videos_context( + course_block, + transcript_languages, + videos, + default_video_image_url + ) del course_videos_context['context_course'] serializer = CourseVideosSerializer(course_videos_context) return Response(serializer.data) @@ -145,11 +150,11 @@ def get(self, request: Request, course_id: str, edx_video_id: str): if not has_studio_read_access(request.user, course_key): self.permission_denied(request) - - download_link = generate_video_download_link(request, course_key, edx_video_id) + + download_link = generate_video_download_link(request, edx_video_id) serializer = VideoDownloadSerializer(download_link) return Response(serializer.data) - + @view_auth_classes(is_authenticated=True) class VideoUsageView(DeveloperErrorViewMixin, APIView): @@ -189,31 +194,10 @@ def get(self, request: Request, course_id: str, edx_video_id: str): 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) - print('LOCATION!!!!! \n', usage_locations) serializer = VideoUsageSerializer(usage_locations) return Response(serializer.data) - - -@view_auth_classes() -class TranscriptCredentialsView(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", - }, - ) - def post(self, request: Request, course_id: str): - pass @view_auth_classes() diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 8437eb5fb124..78624da0fb79 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1610,9 +1610,7 @@ def get_course_videos_context(course_block, transcript_languages, videos, defaul }, 'pagination_context': pagination_context } - print('got contessxt') if is_video_transcript_enabled: - print('transcripts enabled') course_video_context['video_transcript_settings'].update({ 'transcript_preferences_handler_url': reverse_course_url( 'transcript_preferences_handler', @@ -1627,7 +1625,6 @@ def get_course_videos_context(course_block, transcript_languages, videos, defaul course_video_context['active_transcript_preferences'] = get_transcript_preferences(str(course_block.id)) # Cached state for transcript providers' credentials (org-specific) course_video_context['transcript_credentials'] = get_transcript_credentials_state_for_org(course_block.id.org) - print(course_video_context['video_transcript_settings']) return course_video_context diff --git a/cms/djangoapps/contentstore/video_storage_handlers.py b/cms/djangoapps/contentstore/video_storage_handlers.py index f10528f10cbb..9455de4ebbd5 100644 --- a/cms/djangoapps/contentstore/video_storage_handlers.py +++ b/cms/djangoapps/contentstore/video_storage_handlers.py @@ -54,7 +54,7 @@ ) from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.video_block.video_utils import rewrite_video_url # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.video_block.video_utils import rewrite_video_url # lint-amnesty, pylint: disable=wrong-import-order from .models import VideoUploadConfig @@ -226,8 +226,12 @@ def handle_videos(request, course_key_string, edx_video_id=None): data, status = videos_post(course, request) 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() @@ -250,26 +254,32 @@ def get_video_usage_path(request, course_key, edx_video_id): return {'usage_locations': usage_locations} -def generate_video_download_link(request, course_key, edx_video_id): +def generate_video_download_link(request, edx_video_id): + """ + API for fetching a video download link. Returns an url that can be used to download a video. + """ try: import edxval.api as edxval_api except ImportError: edxval_api = None + + from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService + download_video_link = '' + # Determine if there is an alternative source for this video # based on user locale. This exists to support cases where # we leverage a geography specific CDN, like China. default_cdn_url = getattr(settings, 'VIDEO_CDN_URL', {}).get('default') - print(request.user, request) - # user_location = request.user.get_current_user().opt_attrs[ATTR_KEY_REQUEST_COUNTRY_CODE] - cdn_url = getattr(settings, 'VIDEO_CDN_URL', {}).get(None, default_cdn_url) + user = DjangoXBlockUserService(request.user) + user_location = user.get_current_user().opt_attrs[ATTR_KEY_REQUEST_COUNTRY_CODE] + cdn_url = getattr(settings, 'VIDEO_CDN_URL', {}).get(user_location, default_cdn_url) + if edx_video_id and edxval_api: # lint-amnesty, pylint: disable=too-many-nested-blocks + # code partially taken from xmodule/video_block/video_block.py -> get_html() try: val_profiles = ["youtube", "desktop_webm", "desktop_mp4"] - # if HLSPlaybackEnabledFlag.feature_enabled(self.course_id): - # val_profiles.append('hls') - # strip edx_video_id to prevent ValVideoNotFoundError error if unwanted spaces are there. TNL-5769 val_video_urls = edxval_api.get_urls_for_profiles(edx_video_id.strip(), val_profiles) @@ -294,7 +304,7 @@ def generate_video_download_link(request, course_key, edx_video_id): # VAL raises this exception if it can't find data for the edx video ID. This can happen if the # course data is ported to a machine that does not have the VAL data. So for now, pass on this # exception and fallback to whatever we find in the VideoBlock. - log.warning("Could not retrieve information from VAL for edx Video ID: %s.", edx_video_id) + LOGGER.warning("Could not retrieve information from VAL for edx Video ID: %s.", edx_video_id) return {"download_link": download_video_link} From 9399b69aad3bb48ed40f15eae8daafdcd9a72547 Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Wed, 18 Oct 2023 12:19:24 -0400 Subject: [PATCH 03/11] fix: lint errors --- .../contentstore/rest_api/v1/serializers/__init__.py | 2 +- cms/djangoapps/contentstore/video_storage_handlers.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index 439e871aaf37..24e2e4d77af0 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -20,6 +20,6 @@ VideoImageSerializer, VideoDownloadSerializer, VideoUsageSerializer - ) +) from .transcripts import TranscriptSerializer from .assets import AssetSerializer diff --git a/cms/djangoapps/contentstore/video_storage_handlers.py b/cms/djangoapps/contentstore/video_storage_handlers.py index 9455de4ebbd5..092cc54926b0 100644 --- a/cms/djangoapps/contentstore/video_storage_handlers.py +++ b/cms/djangoapps/contentstore/video_storage_handlers.py @@ -18,7 +18,6 @@ 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 @@ -59,7 +58,7 @@ 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, get_course_videos_context +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 @@ -226,7 +225,7 @@ def handle_videos(request, course_key_string, edx_video_id=None): data, status = videos_post(course, request) 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. @@ -271,7 +270,7 @@ def generate_video_download_link(request, edx_video_id): # based on user locale. This exists to support cases where # we leverage a geography specific CDN, like China. default_cdn_url = getattr(settings, 'VIDEO_CDN_URL', {}).get('default') - user = DjangoXBlockUserService(request.user) + user = DjangoXBlockUserService(request.user) user_location = user.get_current_user().opt_attrs[ATTR_KEY_REQUEST_COUNTRY_CODE] cdn_url = getattr(settings, 'VIDEO_CDN_URL', {}).get(user_location, default_cdn_url) From 76a0f4e9062ab683c7e2914733fcf20fd09e2238 Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Tue, 24 Oct 2023 13:09:58 -0400 Subject: [PATCH 04/11] feat: add tests for videos rest_api --- .../rest_api/v1/views/tests/test_videos.py | 130 ++++++++++++++++++ .../contentstore/rest_api/v1/views/videos.py | 66 ++++++++- 2 files changed, 189 insertions(+), 7 deletions(-) create mode 100644 cms/djangoapps/contentstore/rest_api/v1/views/tests/test_videos.py 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..5451eb5005bd --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_videos.py @@ -0,0 +1,130 @@ +""" +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 xmodule.video_block.transcripts_utils import Transcript + +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 fc08db759bfd..dcb6514fb8ae 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py @@ -82,12 +82,65 @@ def get(self, request: Request, course_id: str): **Example Response** ```json { - "allow_unicode_course_id": False, - "course_creator_status": "granted", - "number": "101", - "display_name": "new edx course", - "org": "edx", - "run": "2023", + 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', + }, + { + edx_video_id: 'mOckID5', + clientVideoId: 'mOckID5.mp4', + created: '', + courseVideoImageUrl: 'http:/video', + transcripts: ['en'], + status: 'Failed', + }, + { + edx_video_id: 'mOckID3', + clientVideoId: 'mOckID3.mp4', + created: '', + courseVideoImageUrl: null, + transcripts: ['en'], + status: 'Ready', + }, + ], + 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: {}, } ``` """ @@ -107,7 +160,6 @@ def get(self, request: Request, course_id: str): videos, default_video_image_url ) - del course_videos_context['context_course'] serializer = CourseVideosSerializer(course_videos_context) return Response(serializer.data) From 78760d6185ca24f5106125bd0bb52c157ef7491e Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Wed, 25 Oct 2023 15:39:01 -0400 Subject: [PATCH 05/11] fix: formatting errors --- .../contentstore/rest_api/v1/views/tests/test_videos.py | 2 -- 1 file changed, 2 deletions(-) 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 index 5451eb5005bd..d5d277c498dc 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_videos.py @@ -18,7 +18,6 @@ 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 xmodule.video_block.transcripts_utils import Transcript from ...mixins import PermissionAccessMixin @@ -90,7 +89,6 @@ def test_video_image_upload_enabled(self): 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. From df30a09a15114844f7988a8489b44d088aa142e2 Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Wed, 25 Oct 2023 19:01:53 -0400 Subject: [PATCH 06/11] feat: update video fetch to include size and url --- .../rest_api/v1/serializers/__init__.py | 1 - .../rest_api/v1/serializers/videos.py | 7 +- .../contentstore/rest_api/v1/urls.py | 6 -- .../rest_api/v1/views/__init__.py | 1 - .../contentstore/rest_api/v1/views/videos.py | 52 ++------------- .../contentstore/video_storage_handlers.py | 64 +++---------------- 6 files changed, 16 insertions(+), 115 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index 24e2e4d77af0..2d65d60139a7 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -18,7 +18,6 @@ CourseVideosSerializer, VideoUploadSerializer, VideoImageSerializer, - VideoDownloadSerializer, VideoUsageSerializer ) from .transcripts import TranscriptSerializer diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py index 734b1947f110..657e4339b8cd 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/videos.py @@ -47,6 +47,8 @@ class VideoModelSerializer(serializers.Serializer): 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() ) @@ -87,11 +89,6 @@ class CourseVideosSerializer(serializers.Serializer): ) -class VideoDownloadSerializer(serializers.Serializer): - """Serializer for video download""" - download_link = serializers.CharField() - - class VideoUsageSerializer(serializers.Serializer): """Serializer for video usage""" usage_locations = serializers.ListField( diff --git a/cms/djangoapps/contentstore/rest_api/v1/urls.py b/cms/djangoapps/contentstore/rest_api/v1/urls.py index 2dabca571718..b9f68aa3e982 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v1/urls.py @@ -20,7 +20,6 @@ videos, transcripts, HelpUrlsView, - VideoDownloadView, VideoUsageView ) @@ -39,11 +38,6 @@ CourseVideosView.as_view(), name="course_videos" ), - re_path( - fr'^videos/{COURSE_ID_PATTERN}/{VIDEO_ID_PATTERN}$', - VideoDownloadView.as_view(), - name="video_download" - ), re_path( fr'^videos/{COURSE_ID_PATTERN}/{VIDEO_ID_PATTERN}/usage$', VideoUsageView.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 c78e6eda145f..dfa87a4a34b3 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py @@ -17,7 +17,6 @@ VideoImagesView, VideoEncodingsDownloadView, VideoFeaturesView, - VideoDownloadView, VideoUsageView, ) from .help_urls import HelpUrlsView diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py index dcb6514fb8ae..8301971facfb 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py @@ -34,14 +34,12 @@ handle_video_images, enabled_video_features, _get_index_videos, - generate_video_download_link, get_video_usage_path ) from cms.djangoapps.contentstore.rest_api.v1.serializers import ( CourseVideosSerializer, VideoUploadSerializer, VideoImageSerializer, - VideoDownloadSerializer, VideoUsageSerializer, ) from cms.djangoapps.contentstore.video_storage_handlers import get_all_transcript_languages @@ -94,6 +92,8 @@ def get(self, request: Request, course_id: str): courseVideoImageUrl: '/video', transcripts: [], status: 'Imported', + file_size: 123, + download_link: 'http:/download_video.com' }, { edx_video_id: 'mOckID5', @@ -102,6 +102,8 @@ def get(self, request: Request, course_id: str): courseVideoImageUrl: 'http:/video', transcripts: ['en'], status: 'Failed', + file_size: 0, + download_link: '' }, { edx_video_id: 'mOckID3', @@ -110,6 +112,8 @@ def get(self, request: Request, course_id: str): courseVideoImageUrl: null, transcripts: ['en'], status: 'Ready', + file_size: 123, + download_link: 'http:/download_video.com' }, ], concurrent_upload_limit: 4, @@ -164,50 +168,6 @@ def get(self, request: Request, course_id: str): return Response(serializer.data) -@view_auth_classes(is_authenticated=True) -class VideoDownloadView(DeveloperErrorViewMixin, APIView): - """ - View for course video download. - """ - @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: VideoDownloadSerializer, - 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 - { - "download_link": "http://test-download.com/id", - } - ``` - """ - course_key = CourseKey.from_string(course_id) - - if not has_studio_read_access(request.user, course_key): - self.permission_denied(request) - - download_link = generate_video_download_link(request, edx_video_id) - serializer = VideoDownloadSerializer(download_link) - return Response(serializer.data) - - @view_auth_classes(is_authenticated=True) class VideoUsageView(DeveloperErrorViewMixin, APIView): """ diff --git a/cms/djangoapps/contentstore/video_storage_handlers.py b/cms/djangoapps/contentstore/video_storage_handlers.py index 092cc54926b0..0008df0c02e2 100644 --- a/cms/djangoapps/contentstore/video_storage_handlers.py +++ b/cms/djangoapps/contentstore/video_storage_handlers.py @@ -253,60 +253,6 @@ def get_video_usage_path(request, course_key, edx_video_id): return {'usage_locations': usage_locations} -def generate_video_download_link(request, edx_video_id): - """ - API for fetching a video download link. Returns an url that can be used to download a video. - """ - try: - import edxval.api as edxval_api - except ImportError: - edxval_api = None - - from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService - - download_video_link = '' - - # Determine if there is an alternative source for this video - # based on user locale. This exists to support cases where - # we leverage a geography specific CDN, like China. - default_cdn_url = getattr(settings, 'VIDEO_CDN_URL', {}).get('default') - user = DjangoXBlockUserService(request.user) - user_location = user.get_current_user().opt_attrs[ATTR_KEY_REQUEST_COUNTRY_CODE] - cdn_url = getattr(settings, 'VIDEO_CDN_URL', {}).get(user_location, default_cdn_url) - - if edx_video_id and edxval_api: # lint-amnesty, pylint: disable=too-many-nested-blocks - # code partially taken from xmodule/video_block/video_block.py -> get_html() - try: - val_profiles = ["youtube", "desktop_webm", "desktop_mp4"] - - # strip edx_video_id to prevent ValVideoNotFoundError error if unwanted spaces are there. TNL-5769 - val_video_urls = edxval_api.get_urls_for_profiles(edx_video_id.strip(), val_profiles) - - # VAL will always give us the keys for the profiles we asked for, but - # if it doesn't have an encoded video entry for that Video + Profile, the - # value will map to `None` - - # add the non-youtube urls to the list of alternative sources - # use the last non-None non-youtube non-hls url as the link to download the video - for url in [val_video_urls[p] for p in val_profiles if p != "youtube"]: - if url: - # don't include hls urls for download - if not url.endswith('.m3u8'): - # function returns None when the url cannot be re-written - rewritten_link = rewrite_video_url(cdn_url, url) - if rewritten_link: - download_video_link = rewritten_link - else: - download_video_link = url - - except (edxval_api.ValInternalError, edxval_api.ValVideoNotFoundError): - # VAL raises this exception if it can't find data for the edx video ID. This can happen if the - # course data is ported to a machine that does not have the VAL data. So for now, pass on this - # exception and fallback to whatever we find in the VideoBlock. - LOGGER.warning("Could not retrieve information from VAL for edx Video ID: %s.", edx_video_id) - return {"download_link": download_video_link} - - 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 @@ -669,7 +615,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' ] @@ -682,9 +628,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()) + if 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) From 864608068345b7bffb1fe63f6944d8a49fa0050e Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Wed, 25 Oct 2023 21:07:51 -0400 Subject: [PATCH 07/11] feat: video tests --- .../contentstore/video_storage_handlers.py | 5 +---- .../contentstore/views/tests/test_videos.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/cms/djangoapps/contentstore/video_storage_handlers.py b/cms/djangoapps/contentstore/video_storage_handlers.py index 0008df0c02e2..a4380c45e977 100644 --- a/cms/djangoapps/contentstore/video_storage_handlers.py +++ b/cms/djangoapps/contentstore/video_storage_handlers.py @@ -43,7 +43,6 @@ from common.djangoapps.edxmako.shortcuts import render_to_response from common.djangoapps.student.auth import has_course_author_access -from common.djangoapps.xblock_django.constants import ATTR_KEY_REQUEST_COUNTRY_CODE 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 @@ -53,8 +52,6 @@ ) from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.video_block.video_utils import rewrite_video_url # lint-amnesty, pylint: disable=wrong-import-order - from .models import VideoUploadConfig from .toggles import use_new_video_uploads_page, use_mock_video_uploads @@ -628,7 +625,7 @@ 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()) - if attr == 'encoded_videos': + elif attr == 'encoded_videos': values['download_link'] = '' values['file_size'] = 0 for encoding in video['encoded_videos']: 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) From f0a7823b451c2485f24263e8cbd2f4124b78b643 Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Thu, 26 Oct 2023 10:37:19 -0400 Subject: [PATCH 08/11] fix: cleanup drf api --- .../contentstore/rest_api/v1/views/videos.py | 7 +------ cms/djangoapps/contentstore/utils.py | 14 ++++++++++---- .../contentstore/video_storage_handlers.py | 7 +------ 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py index 8301971facfb..50342496d08e 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py @@ -153,16 +153,11 @@ def get(self, request: Request, course_id: str): if not has_studio_read_access(request.user, course_key): self.permission_denied(request) - transcript_languages = get_all_transcript_languages() - default_video_image_url = staticfiles_storage.url(settings.VIDEO_IMAGE_DEFAULT_FILENAME) with modulestore().bulk_operations(course_key): course_block = modulestore().get_course(course_key) - videos = _get_index_videos(course_block) course_videos_context = get_course_videos_context( course_block, - transcript_languages, - videos, - default_video_image_url + None, ) serializer = CourseVideosSerializer(course_videos_context) return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index d6887b0d1840..a988ab3a6681 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1568,7 +1568,7 @@ def get_course_rerun_context(course_key, course_block, user): return course_rerun_context -def get_course_videos_context(course_block, transcript_languages, videos, default_video_image_url): +def get_course_videos_context(course_block, pagination_conf): """ Utils is used to get contest of course videos. It is used for both DRF and django views. @@ -1583,6 +1583,12 @@ def get_course_videos_context(course_block, transcript_languages, videos, defaul 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', @@ -1594,13 +1600,13 @@ def get_course_videos_context(course_block, transcript_languages, videos, defaul ) is_video_transcript_enabled = VideoTranscriptEnabledFlag.feature_enabled(course_block.id) - previous_uploads, pagination_context = videos + previous_uploads, pagination_context = _get_index_videos(course_block, pagination_conf) course_video_context = { 'context_course': course_block, 'image_upload_url': reverse_course_url('video_images_handler', str(course_block.id)), 'video_handler_url': reverse_course_url('videos_handler', str(course_block.id)), 'encodings_download_url': reverse_course_url('video_encodings_download', str(course_block.id)), - 'default_video_image_url': default_video_image_url, + '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()), @@ -1616,7 +1622,7 @@ def get_course_videos_context(course_block, transcript_languages, videos, defaul 'is_video_transcript_enabled': is_video_transcript_enabled, 'active_transcript_preferences': None, 'transcript_credentials': None, - 'transcript_available_languages': transcript_languages, + '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'), diff --git a/cms/djangoapps/contentstore/video_storage_handlers.py b/cms/djangoapps/contentstore/video_storage_handlers.py index a4380c45e977..318a91b4bb79 100644 --- a/cms/djangoapps/contentstore/video_storage_handlers.py +++ b/cms/djangoapps/contentstore/video_storage_handlers.py @@ -669,14 +669,9 @@ def videos_index_html(course, pagination_conf=None): """ Returns an HTML page to display previous video uploads and allow new ones """ - videos = _get_index_videos(course, pagination_conf) - transcript_languages = get_all_transcript_languages() - default_video_image_url = _get_default_video_image_url() context = get_course_videos_context( course, - transcript_languages, - videos, - default_video_image_url, + pagination_conf, ) if use_new_video_uploads_page(course.id): return redirect(get_video_uploads_url(course.id)) From e7579f0c91a9211e90ee6e1d4ab009dc8bc932ec Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Thu, 26 Oct 2023 12:35:29 -0400 Subject: [PATCH 09/11] fix: remove unused imports --- cms/djangoapps/contentstore/rest_api/v1/views/videos.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py index 50342496d08e..e7939602c83f 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py @@ -13,8 +13,6 @@ from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.parsers import (MultiPartParser, FormParser) -from django.conf import settings -from django.contrib.staticfiles.storage import staticfiles_storage from django.views.decorators.csrf import csrf_exempt from django.http import Http404 @@ -27,13 +25,11 @@ 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, - _get_index_videos, get_video_usage_path ) from cms.djangoapps.contentstore.rest_api.v1.serializers import ( @@ -42,7 +38,6 @@ VideoImageSerializer, VideoUsageSerializer, ) -from cms.djangoapps.contentstore.video_storage_handlers import get_all_transcript_languages import cms.djangoapps.contentstore.toggles as contentstore_toggles from .utils import validate_request_with_serializer From e514760dee73ee10d2ba64030cee7adce55b35ec Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Thu, 26 Oct 2023 15:32:12 -0400 Subject: [PATCH 10/11] feat: remove modulestore call from drf --- .../contentstore/rest_api/v1/views/videos.py | 12 +++---- cms/djangoapps/contentstore/utils.py | 31 +++++++++++-------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py index e7939602c83f..d4ea444bd134 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/videos.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/videos.py @@ -20,7 +20,6 @@ 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 xmodule.modulestore.django import modulestore from ....api import course_author_access_required from ....utils import get_course_videos_context @@ -148,12 +147,11 @@ def get(self, request: Request, course_id: str): if not has_studio_read_access(request.user, course_key): self.permission_denied(request) - with modulestore().bulk_operations(course_key): - course_block = modulestore().get_course(course_key) - course_videos_context = get_course_videos_context( - course_block, - None, - ) + course_videos_context = get_course_videos_context( + None, + None, + course_key, + ) serializer = CourseVideosSerializer(course_videos_context) return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index a988ab3a6681..6abaaa425322 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1568,7 +1568,7 @@ def get_course_rerun_context(course_key, course_block, user): return course_rerun_context -def get_course_videos_context(course_block, pagination_conf): +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. @@ -1598,14 +1598,19 @@ def get_course_videos_context(course_block, pagination_conf): VIDEO_IMAGE_UPLOAD_ENABLED = WaffleSwitch( # lint-amnesty, pylint: disable=toggle-missing-annotation 'videos.video_image_upload_enabled', __name__ ) - - is_video_transcript_enabled = VideoTranscriptEnabledFlag.feature_enabled(course_block.id) - previous_uploads, pagination_context = _get_index_videos(course_block, pagination_conf) + + 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_block, - 'image_upload_url': reverse_course_url('video_images_handler', str(course_block.id)), - 'video_handler_url': reverse_course_url('videos_handler', str(course_block.id)), - 'encodings_download_url': reverse_course_url('video_encodings_download', str(course_block.id)), + '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), @@ -1626,7 +1631,7 @@ def get_course_videos_context(course_block, pagination_conf): '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_block.id)), + 'transcript_delete_handler_url': reverse_course_url('transcript_delete_handler', str(course.id)), 'trancript_download_file_format': Transcript.SRT }, 'pagination_context': pagination_context @@ -1635,17 +1640,17 @@ def get_course_videos_context(course_block, pagination_conf): course_video_context['video_transcript_settings'].update({ 'transcript_preferences_handler_url': reverse_course_url( 'transcript_preferences_handler', - str(course_block.id) + str(course.id) ), 'transcript_credentials_handler_url': reverse_course_url( 'transcript_credentials_handler', - str(course_block.id) + str(course.id) ), 'transcription_plans': get_3rd_party_transcription_plans(), }) - course_video_context['active_transcript_preferences'] = get_transcript_preferences(str(course_block.id)) + 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_block.id.org) + course_video_context['transcript_credentials'] = get_transcript_credentials_state_for_org(course.id.org) return course_video_context From 3042ddc911cf08960bf0ece998871897a82ca69c Mon Sep 17 00:00:00 2001 From: KristinAoki Date: Thu, 26 Oct 2023 15:58:37 -0400 Subject: [PATCH 11/11] fix: formatting errors --- cms/djangoapps/contentstore/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 6abaaa425322..48fc2c962cf1 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1598,7 +1598,7 @@ def get_course_videos_context(course_block, pagination_conf, course_key=None): 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):