feat: add drf for studio video page (#33528)

This commit is contained in:
Kristin Aoki
2023-10-27 09:04:37 -04:00
committed by GitHub
parent 3c50f3e029
commit 8cfd04ff68
9 changed files with 543 additions and 64 deletions

View File

@@ -14,6 +14,11 @@ from .proctoring import (
)
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

View File

@@ -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.

View File

@@ -11,6 +11,7 @@ from .views import (
CourseGradingView,
CourseRerunView,
CourseSettingsView,
CourseVideosView,
HomePageView,
ProctoredExamSettingsView,
ProctoringErrorsView,
@@ -19,6 +20,7 @@ from .views import (
videos,
transcripts,
HelpUrlsView,
VideoUsageView
)
app_name = 'v1'
@@ -31,6 +33,16 @@ urlpatterns = [
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(),

View File

@@ -11,10 +11,12 @@ from .settings import CourseSettingsView
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

View File

@@ -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"])

View File

@@ -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 @@ log = logging.getLogger(__name__)
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):
"""